mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2026-01-31 10:25:03 -06:00
Merge branch 'feature/go_router_migration' into development
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
<application android:label="Paperless Mobile"
|
<application android:label="Paperless Mobile"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true"
|
||||||
|
>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
||||||
|
After Width: | Height: | Size: 963 B |
@@ -1,3 +1,4 @@
|
|||||||
|
project_id: "568557"
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
"source" : "/lib/l10n/intl_en.arb",
|
"source" : "/lib/l10n/intl_en.arb",
|
||||||
|
|||||||
+39
-34
@@ -35,7 +35,7 @@ PODS:
|
|||||||
- DKPhotoGallery/Resource (0.0.17):
|
- DKPhotoGallery/Resource (0.0.17):
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- edge_detection (1.1.1):
|
- edge_detection (1.1.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WeScan
|
- WeScan
|
||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
@@ -48,14 +48,16 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (0.0.1):
|
- flutter_native_splash (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_pdfview (1.0.2):
|
||||||
|
- Flutter
|
||||||
|
- flutter_secure_storage (6.0.0):
|
||||||
|
- Flutter
|
||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Toast
|
- Toast
|
||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
- in_app_review (0.2.0):
|
|
||||||
- Flutter
|
|
||||||
- integration_test (0.0.1):
|
- integration_test (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- local_auth_ios (0.0.1):
|
- local_auth_ios (0.0.1):
|
||||||
@@ -67,9 +69,9 @@ PODS:
|
|||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- pdfx (1.0.0):
|
- permission_handler_apple (9.1.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- permission_handler_apple (9.0.4):
|
- printing (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift (5.0.0)
|
- ReachabilitySwift (5.0.0)
|
||||||
- receive_sharing_intent (0.0.1):
|
- receive_sharing_intent (0.0.1):
|
||||||
@@ -79,16 +81,15 @@ 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_foundation (0.0.1):
|
- sqflite (0.0.3):
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- sqflite (0.0.2):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FMDB (>= 2.7.5)
|
||||||
- SwiftyGif (5.4.3)
|
- SwiftyGif (5.4.3)
|
||||||
- Toast (4.0.0)
|
- Toast (4.0.0)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
|
- Flutter
|
||||||
- WeScan (1.7.0)
|
- WeScan (1.7.0)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
@@ -100,20 +101,21 @@ DEPENDENCIES:
|
|||||||
- 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_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`)
|
||||||
|
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
|
||||||
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
- in_app_review (from `.symlinks/plugins/in_app_review/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`)
|
- 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_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- printing (from `.symlinks/plugins/printing/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_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
|
||||||
- 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`)
|
||||||
|
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -143,10 +145,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
: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"
|
||||||
|
flutter_pdfview:
|
||||||
|
:path: ".symlinks/plugins/flutter_pdfview/ios"
|
||||||
|
flutter_secure_storage:
|
||||||
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
in_app_review:
|
|
||||||
:path: ".symlinks/plugins/in_app_review/ios"
|
|
||||||
integration_test:
|
integration_test:
|
||||||
:path: ".symlinks/plugins/integration_test/ios"
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
local_auth_ios:
|
local_auth_ios:
|
||||||
@@ -156,54 +160,55 @@ EXTERNAL SOURCES:
|
|||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
pdfx:
|
|
||||||
:path: ".symlinks/plugins/pdfx/ios"
|
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
printing:
|
||||||
|
:path: ".symlinks/plugins/printing/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
: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_foundation:
|
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
|
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||||
edge_detection: fa02aa120e00d87ada0ca2430b6c6087a501b1e9
|
edge_detection: b4fb239b018cefa79515a024d0bf3e559336de4e
|
||||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
flutter_pdfview: 25f53dd6097661e6395b17de506e6060585946bd
|
||||||
|
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||||
|
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
|
||||||
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
|
||||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||||
pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec
|
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||||
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
|
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||||
SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370
|
SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370
|
||||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
|
||||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
|
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||||
|
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||||
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13
|
PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.12.0
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
|
||||||
|
|
||||||
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
|
|
||||||
DocumentStatusCubit() : super(null);
|
|
||||||
|
|
||||||
void updateStatus(DocumentProcessingStatus? status) => emit(status);
|
|
||||||
}
|
|
||||||
@@ -15,11 +15,9 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|||||||
class HiveBoxes {
|
class HiveBoxes {
|
||||||
HiveBoxes._();
|
HiveBoxes._();
|
||||||
static const globalSettings = 'globalSettings';
|
static const globalSettings = 'globalSettings';
|
||||||
static const authentication = 'authentication';
|
|
||||||
static const localUserCredentials = 'localUserCredentials';
|
static const localUserCredentials = 'localUserCredentials';
|
||||||
static const localUserAccount = 'localUserAccount';
|
static const localUserAccount = 'localUserAccount';
|
||||||
static const localUserAppState = 'localUserAppState';
|
static const localUserAppState = 'localUserAppState';
|
||||||
static const localUserSettings = 'localUserSettings';
|
|
||||||
static const hosts = 'hosts';
|
static const hosts = 'hosts';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Opens an encrypted box, calls [callback] with the now opened box, awaits
|
/// Opens an encrypted box, calls [callback] with the now opened box, awaits
|
||||||
/// [callback] to return and returns the calculated value. Closes the box after.
|
/// [callback] to return and returns the calculated value. Closes the box after.
|
||||||
///
|
///
|
||||||
Future<R?> withEncryptedBox<T, R>(
|
Future<R?> withEncryptedBox<T, R>(
|
||||||
String name, FutureOr<R?> Function(Box<T> box) callback) async {
|
String name,
|
||||||
|
FutureOr<R?> Function(Box<T> box) callback,
|
||||||
|
) async {
|
||||||
final key = await _getEncryptedBoxKey();
|
final key = await _getEncryptedBoxKey();
|
||||||
final box = await Hive.openBox<T>(
|
final box = await Hive.openBox<T>(
|
||||||
name,
|
name,
|
||||||
@@ -22,7 +28,11 @@ Future<R?> withEncryptedBox<T, R>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> _getEncryptedBoxKey() async {
|
Future<Uint8List> _getEncryptedBoxKey() async {
|
||||||
const secureStorage = FlutterSecureStorage();
|
const secureStorage = FlutterSecureStorage(
|
||||||
|
aOptions: AndroidOptions(
|
||||||
|
encryptedSharedPreferences: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
if (!await secureStorage.containsKey(key: 'key')) {
|
if (!await secureStorage.containsKey(key: 'key')) {
|
||||||
final key = Hive.generateSecureKey();
|
final key = Hive.generateSecureKey();
|
||||||
|
|
||||||
@@ -34,3 +44,14 @@ Future<Uint8List> _getEncryptedBoxKey() async {
|
|||||||
final key = (await secureStorage.read(key: 'key'))!;
|
final key = (await secureStorage.read(key: 'key'))!;
|
||||||
return base64Decode(key);
|
return base64Decode(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension HiveBoxAccessors on HiveInterface {
|
||||||
|
Box<GlobalSettings> get settingsBox =>
|
||||||
|
box<GlobalSettings>(HiveBoxes.globalSettings);
|
||||||
|
Box<LocalUserAccount> get localUserAccountBox =>
|
||||||
|
box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||||
|
Box<LocalUserAppState> get localUserAppStateBox =>
|
||||||
|
box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||||
|
Box<GlobalSettings> get globalSettingsBox =>
|
||||||
|
box<GlobalSettings>(HiveBoxes.globalSettings);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class GlobalSettings with HiveObjectMixin {
|
|||||||
bool showOnboarding;
|
bool showOnboarding;
|
||||||
|
|
||||||
@HiveField(4)
|
@HiveField(4)
|
||||||
String? currentLoggedInUser;
|
String? loggedInUserId;
|
||||||
|
|
||||||
@HiveField(5)
|
@HiveField(5)
|
||||||
FileDownloadType defaultDownloadType;
|
FileDownloadType defaultDownloadType;
|
||||||
@@ -32,14 +32,18 @@ class GlobalSettings with HiveObjectMixin {
|
|||||||
@HiveField(7, defaultValue: false)
|
@HiveField(7, defaultValue: false)
|
||||||
bool enforceSinglePagePdfUpload;
|
bool enforceSinglePagePdfUpload;
|
||||||
|
|
||||||
|
@HiveField(8, defaultValue: false)
|
||||||
|
bool skipDocumentPreprarationOnUpload;
|
||||||
|
|
||||||
GlobalSettings({
|
GlobalSettings({
|
||||||
required this.preferredLocaleSubtag,
|
required this.preferredLocaleSubtag,
|
||||||
this.preferredThemeMode = ThemeMode.system,
|
this.preferredThemeMode = ThemeMode.system,
|
||||||
this.preferredColorSchemeOption = ColorSchemeOption.classic,
|
this.preferredColorSchemeOption = ColorSchemeOption.classic,
|
||||||
this.showOnboarding = true,
|
this.showOnboarding = true,
|
||||||
this.currentLoggedInUser,
|
this.loggedInUserId,
|
||||||
this.defaultDownloadType = FileDownloadType.alwaysAsk,
|
this.defaultDownloadType = FileDownloadType.alwaysAsk,
|
||||||
this.defaultShareType = FileDownloadType.alwaysAsk,
|
this.defaultShareType = FileDownloadType.alwaysAsk,
|
||||||
this.enforceSinglePagePdfUpload = false,
|
this.enforceSinglePagePdfUpload = false,
|
||||||
|
this.skipDocumentPreprarationOnUpload = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
||||||
|
|
||||||
part 'local_user_account.g.dart';
|
part 'local_user_account.g.dart';
|
||||||
|
|
||||||
@@ -20,16 +19,16 @@ class LocalUserAccount extends HiveObject {
|
|||||||
@HiveField(7)
|
@HiveField(7)
|
||||||
UserModel paperlessUser;
|
UserModel paperlessUser;
|
||||||
|
|
||||||
|
@HiveField(8, defaultValue: 2)
|
||||||
|
int apiVersion;
|
||||||
|
|
||||||
LocalUserAccount({
|
LocalUserAccount({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.serverUrl,
|
required this.serverUrl,
|
||||||
required this.settings,
|
required this.settings,
|
||||||
required this.paperlessUser,
|
required this.paperlessUser,
|
||||||
|
required this.apiVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
static LocalUserAccount get current =>
|
bool get hasMultiUserSupport => apiVersion >= 3;
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(
|
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
|
||||||
.getValue()!
|
|
||||||
.currentLoggedInUser)!;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class LocalUserAppState extends HiveObject {
|
|||||||
final currentLocalUserId =
|
final currentLocalUserId =
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||||
.getValue()!
|
.getValue()!
|
||||||
.currentLoggedInUser!;
|
.loggedInUserId!;
|
||||||
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
return Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
||||||
.get(currentLocalUserId)!;
|
.get(currentLocalUserId)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,8 @@
|
|||||||
const supportedFileExtensions = ['pdf', 'png', 'tiff', 'gif', 'jpg', 'jpeg'];
|
const supportedFileExtensions = [
|
||||||
|
'.pdf',
|
||||||
|
'.png',
|
||||||
|
'.tiff',
|
||||||
|
'.gif',
|
||||||
|
'.jpg',
|
||||||
|
'.jpeg'
|
||||||
|
];
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ class DioHttpErrorInterceptor extends Interceptor {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return handler.next(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
|
class InfoMessageException implements Exception {
|
||||||
|
final ErrorCode code;
|
||||||
|
final String? message;
|
||||||
|
final StackTrace? stackTrace;
|
||||||
|
InfoMessageException({
|
||||||
|
required this.code,
|
||||||
|
this.message,
|
||||||
|
this.stackTrace,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.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/user_repository.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.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/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
import 'package:paperless_mobile/routes/document_details_route.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
// These are convenience methods for nativating to views without having to pass providers around explicitly.
|
|
||||||
// Providers unfortunately have to be passed to the routes since they are children of the Navigator, not ancestors.
|
|
||||||
|
|
||||||
Future<void> pushDocumentSearchPage(BuildContext context) {
|
|
||||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
|
||||||
.getValue()!
|
|
||||||
.currentLoggedInUser;
|
|
||||||
final userRepo = context.read<UserRepository>();
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
|
||||||
Provider.value(value: userRepo),
|
|
||||||
],
|
|
||||||
builder: (context, _) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) => DocumentSearchCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
|
||||||
.get(currentUser)!,
|
|
||||||
),
|
|
||||||
child: const DocumentSearchPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushDocumentDetailsRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required DocumentModel document,
|
|
||||||
bool isLabelClickable = true,
|
|
||||||
bool allowEdit = true,
|
|
||||||
String? titleAndContentQueryString,
|
|
||||||
}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: context.read<ApiVersion>()),
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
|
||||||
Provider.value(value: context.read<LocalNotificationService>()),
|
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
|
||||||
Provider.value(value: context.read<ConnectivityCubit>()),
|
|
||||||
if (context.read<ApiVersion>().hasMultiUserSupport)
|
|
||||||
Provider.value(value: context.read<UserRepository>()),
|
|
||||||
],
|
|
||||||
child: DocumentDetailsRoute(
|
|
||||||
document: document,
|
|
||||||
isLabelClickable: isLabelClickable,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushSavedViewDetailsRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required SavedView savedView,
|
|
||||||
}) {
|
|
||||||
final apiVersion = context.read<ApiVersion>();
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: apiVersion),
|
|
||||||
if (apiVersion.hasMultiUserSupport)
|
|
||||||
Provider.value(value: context.read<UserRepository>()),
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
|
||||||
Provider.value(value: context.read<ConnectivityCubit>()),
|
|
||||||
],
|
|
||||||
builder: (_, child) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) => SavedViewDetailsCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
LocalUserAppState.current,
|
|
||||||
savedView: savedView,
|
|
||||||
),
|
|
||||||
child: SavedViewDetailsPage(
|
|
||||||
onDelete: context.read<SavedViewCubit>().remove),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<SavedView?> pushAddSavedViewRoute(BuildContext context,
|
|
||||||
{required DocumentFilter filter}) {
|
|
||||||
return Navigator.of(context).push<SavedView?>(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => AddSavedViewPage(
|
|
||||||
currentFilter: filter,
|
|
||||||
correspondents: context.read<LabelRepository>().state.correspondents,
|
|
||||||
documentTypes: context.read<LabelRepository>().state.documentTypes,
|
|
||||||
storagePaths: context.read<LabelRepository>().state.storagePaths,
|
|
||||||
tags: context.read<LabelRepository>().state.tags,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushLinkedDocumentsView(BuildContext context,
|
|
||||||
{required DocumentFilter filter}) {
|
|
||||||
return Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: context.read<ApiVersion>()),
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
|
||||||
Provider.value(value: context.read<LocalNotificationService>()),
|
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
|
||||||
Provider.value(value: context.read<ConnectivityCubit>()),
|
|
||||||
if (context.read<ApiVersion>().hasMultiUserSupport)
|
|
||||||
Provider.value(value: context.read<UserRepository>()),
|
|
||||||
],
|
|
||||||
builder: (context, _) => BlocProvider(
|
|
||||||
create: (context) => LinkedDocumentsCubit(
|
|
||||||
filter,
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
),
|
|
||||||
child: const LinkedDocumentsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditCorrespondentRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required List<DocumentModel> selection,
|
|
||||||
}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return FullscreenBulkEditLabelPage(
|
|
||||||
options: state.correspondents,
|
|
||||||
selection: state.selection,
|
|
||||||
labelMapper: (document) => document.correspondent,
|
|
||||||
leadingIcon: const Icon(Icons.person_outline),
|
|
||||||
hintText: S.of(context)!.startTyping,
|
|
||||||
onSubmit: context
|
|
||||||
.read<DocumentBulkActionCubit>()
|
|
||||||
.bulkModifyCorrespondent,
|
|
||||||
assignMessageBuilder: (int count, String name) {
|
|
||||||
return S.of(context)!.bulkEditCorrespondentAssignMessage(
|
|
||||||
name,
|
|
||||||
count,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeMessageBuilder: (int count) {
|
|
||||||
return S
|
|
||||||
.of(context)!
|
|
||||||
.bulkEditCorrespondentRemoveMessage(count);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditStoragePathRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required List<DocumentModel> selection,
|
|
||||||
}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return FullscreenBulkEditLabelPage(
|
|
||||||
options: state.storagePaths,
|
|
||||||
selection: state.selection,
|
|
||||||
labelMapper: (document) => document.storagePath,
|
|
||||||
leadingIcon: const Icon(Icons.folder_outlined),
|
|
||||||
hintText: S.of(context)!.startTyping,
|
|
||||||
onSubmit: context
|
|
||||||
.read<DocumentBulkActionCubit>()
|
|
||||||
.bulkModifyStoragePath,
|
|
||||||
assignMessageBuilder: (int count, String name) {
|
|
||||||
return S.of(context)!.bulkEditStoragePathAssignMessage(
|
|
||||||
count,
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeMessageBuilder: (int count) {
|
|
||||||
return S.of(context)!.bulkEditStoragePathRemoveMessage(count);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditTagsRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required List<DocumentModel> selection,
|
|
||||||
}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: Builder(builder: (context) {
|
|
||||||
return const FullscreenBulkEditTagsWidget();
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
|
|
||||||
{required List<DocumentModel> selection}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return FullscreenBulkEditLabelPage(
|
|
||||||
options: state.documentTypes,
|
|
||||||
selection: state.selection,
|
|
||||||
labelMapper: (document) => document.documentType,
|
|
||||||
leadingIcon: const Icon(Icons.description_outlined),
|
|
||||||
hintText: S.of(context)!.startTyping,
|
|
||||||
onSubmit: context
|
|
||||||
.read<DocumentBulkActionCubit>()
|
|
||||||
.bulkModifyDocumentType,
|
|
||||||
assignMessageBuilder: (int count, String name) {
|
|
||||||
return S.of(context)!.bulkEditDocumentTypeAssignMessage(
|
|
||||||
count,
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeMessageBuilder: (int count) {
|
|
||||||
return S
|
|
||||||
.of(context)!
|
|
||||||
.bulkEditDocumentTypeRemoveMessage(count);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
|
|
||||||
BuildContext context, {
|
|
||||||
required Uint8List bytes,
|
|
||||||
String? filename,
|
|
||||||
String? fileExtension,
|
|
||||||
String? title,
|
|
||||||
}) {
|
|
||||||
final labelRepo = context.read<LabelRepository>();
|
|
||||||
final docsApi = context.read<PaperlessDocumentsApi>();
|
|
||||||
final connectivity = context.read<Connectivity>();
|
|
||||||
final apiVersion = context.read<ApiVersion>();
|
|
||||||
return Navigator.of(context).push<DocumentUploadResult>(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: labelRepo),
|
|
||||||
Provider.value(value: docsApi),
|
|
||||||
Provider.value(value: connectivity),
|
|
||||||
Provider.value(value: apiVersion)
|
|
||||||
],
|
|
||||||
builder: (_, child) => BlocProvider(
|
|
||||||
create: (_) => DocumentUploadCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
),
|
|
||||||
child: DocumentUploadPreparationPage(
|
|
||||||
fileBytes: bytes,
|
|
||||||
fileExtension: fileExtension,
|
|
||||||
filename: filename,
|
|
||||||
title: title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Provider> _getRequiredBulkEditProviders(BuildContext context) {
|
|
||||||
return [
|
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,10 @@ class DocumentChangedNotifier {
|
|||||||
|
|
||||||
final Map<dynamic, List<StreamSubscription>> _subscribers = {};
|
final Map<dynamic, List<StreamSubscription>> _subscribers = {};
|
||||||
|
|
||||||
|
Stream<DocumentModel> get $updated => _updated.asBroadcastStream();
|
||||||
|
|
||||||
|
Stream<DocumentModel> get $deleted => _deleted.asBroadcastStream();
|
||||||
|
|
||||||
void notifyUpdated(DocumentModel updated) {
|
void notifyUpdated(DocumentModel updated) {
|
||||||
debugPrint("Notifying updated document ${updated.id}");
|
debugPrint("Notifying updated document ${updated.id}");
|
||||||
_updated.add(updated);
|
_updated.add(updated);
|
||||||
|
|||||||
@@ -8,25 +8,26 @@ abstract class PersistentRepository<T> extends HydratedCubit<T> {
|
|||||||
PersistentRepository(T initialState) : super(initialState);
|
PersistentRepository(T initialState) : super(initialState);
|
||||||
|
|
||||||
void addListener(
|
void addListener(
|
||||||
Object source, {
|
Object subscriber, {
|
||||||
required void Function(T) onChanged,
|
required void Function(T) onChanged,
|
||||||
}) {
|
}) {
|
||||||
onChanged(state);
|
onChanged(state);
|
||||||
_subscribers.putIfAbsent(source, () {
|
_subscribers.putIfAbsent(subscriber, () {
|
||||||
return stream.listen((event) => onChanged(event));
|
return stream.listen((event) => onChanged(event));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeListener(Object source) async {
|
void removeListener(Object source) async {
|
||||||
await _subscribers[source]?.cancel();
|
_subscribers
|
||||||
_subscribers.remove(source);
|
..[source]?.cancel()
|
||||||
|
..remove(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_subscribers.forEach((key, subscription) {
|
for (final subscriber in _subscribers.values) {
|
||||||
subscription.cancel();
|
subscriber.cancel();
|
||||||
});
|
}
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ class SavedViewRepository
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<SavedView> update(SavedView object) async {
|
||||||
|
await _initialized.future;
|
||||||
|
final updated = await _api.update(object);
|
||||||
|
final updatedState = {...state.savedViews}..update(
|
||||||
|
updated.id!,
|
||||||
|
(_) => updated,
|
||||||
|
ifAbsent: () => updated,
|
||||||
|
);
|
||||||
|
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> delete(SavedView view) async {
|
Future<int> delete(SavedView view) async {
|
||||||
await _initialized.future;
|
await _initialized.future;
|
||||||
await _api.delete(view);
|
await _api.delete(view);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||||
|
import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
@@ -37,6 +38,7 @@ class SessionManager extends ValueNotifier<Dio> {
|
|||||||
...interceptors,
|
...interceptors,
|
||||||
DioUnauthorizedInterceptor(),
|
DioUnauthorizedInterceptor(),
|
||||||
DioHttpErrorInterceptor(),
|
DioHttpErrorInterceptor(),
|
||||||
|
DioOfflineInterceptor(),
|
||||||
PrettyDioLogger(
|
PrettyDioLogger(
|
||||||
compact: true,
|
compact: true,
|
||||||
responseBody: false,
|
responseBody: false,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:paperless_mobile/core/interceptor/server_reachability_error_inte
|
|||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||||
|
import 'package:rxdart/subjects.dart';
|
||||||
|
|
||||||
abstract class ConnectivityStatusService {
|
abstract class ConnectivityStatusService {
|
||||||
Future<bool> isConnectedToInternet();
|
Future<bool> isConnectedToInternet();
|
||||||
@@ -20,14 +21,19 @@ abstract class ConnectivityStatusService {
|
|||||||
|
|
||||||
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||||
final Connectivity _connectivity;
|
final Connectivity _connectivity;
|
||||||
|
final BehaviorSubject<bool> _connectivityState$ = BehaviorSubject();
|
||||||
|
|
||||||
ConnectivityStatusServiceImpl(this._connectivity);
|
ConnectivityStatusServiceImpl(this._connectivity) {
|
||||||
|
_connectivityState$.addStream(
|
||||||
|
_connectivity.onConnectivityChanged
|
||||||
|
.map(_hasActiveInternetConnection)
|
||||||
|
.asBroadcastStream(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<bool> connectivityChanges() {
|
Stream<bool> connectivityChanges() {
|
||||||
return _connectivity.onConnectivityChanged
|
return _connectivityState$.asBroadcastStream();
|
||||||
.map(_hasActiveInternetConnection)
|
|
||||||
.asBroadcastStream();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -98,3 +104,31 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
return ReachabilityStatus.notReachable;
|
return ReachabilityStatus.notReachable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ConnectivityStatusServiceMock implements ConnectivityStatusService {
|
||||||
|
final bool isConnected;
|
||||||
|
|
||||||
|
ConnectivityStatusServiceMock(this.isConnected);
|
||||||
|
@override
|
||||||
|
Stream<bool> connectivityChanges() {
|
||||||
|
return Stream.value(isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isConnectedToInternet() async {
|
||||||
|
return isConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ReachabilityStatus> isPaperlessServerReachable(String serverAddress,
|
||||||
|
[ClientCertificate? clientCertificate]) async {
|
||||||
|
return isConnected
|
||||||
|
? ReachabilityStatus.reachable
|
||||||
|
: ReachabilityStatus.notReachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isServerReachable(String serverAddress) async {
|
||||||
|
return isConnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
class FileDescription {
|
|
||||||
final String filename;
|
|
||||||
final String extension;
|
|
||||||
|
|
||||||
FileDescription({
|
|
||||||
required this.filename,
|
|
||||||
required this.extension,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory FileDescription.fromPath(String path) {
|
|
||||||
final filename = path.split(RegExp(r"/")).last;
|
|
||||||
final fragments = filename.split(".");
|
|
||||||
final ext = fragments.removeLast();
|
|
||||||
final name = fragments.join(".");
|
|
||||||
return FileDescription(
|
|
||||||
filename: name,
|
|
||||||
extension: ext,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,30 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class FileService {
|
class FileService {
|
||||||
|
const FileService._();
|
||||||
|
|
||||||
static Future<File> saveToFile(
|
static Future<File> saveToFile(
|
||||||
Uint8List bytes,
|
Uint8List bytes,
|
||||||
String filename,
|
String filename,
|
||||||
) async {
|
) async {
|
||||||
final dir = await documentsDirectory;
|
final dir = await documentsDirectory;
|
||||||
if (dir == null) {
|
|
||||||
throw const PaperlessApiException.unknown(); //TODO: better handling
|
|
||||||
}
|
|
||||||
File file = File("${dir.path}/$filename");
|
File file = File("${dir.path}/$filename");
|
||||||
return file..writeAsBytes(bytes);
|
return file..writeAsBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Directory?> getDirectory(PaperlessDirectoryType type) {
|
static Future<Directory?> getDirectory(PaperlessDirectoryType type) {
|
||||||
switch (type) {
|
return switch (type) {
|
||||||
case PaperlessDirectoryType.documents:
|
PaperlessDirectoryType.documents => documentsDirectory,
|
||||||
return documentsDirectory;
|
PaperlessDirectoryType.temporary => temporaryDirectory,
|
||||||
case PaperlessDirectoryType.temporary:
|
PaperlessDirectoryType.scans => temporaryScansDirectory,
|
||||||
return temporaryDirectory;
|
PaperlessDirectoryType.download => downloadsDirectory,
|
||||||
case PaperlessDirectoryType.scans:
|
PaperlessDirectoryType.upload => uploadDirectory,
|
||||||
return scanDirectory;
|
};
|
||||||
case PaperlessDirectoryType.download:
|
|
||||||
return downloadsDirectory;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<File> allocateTemporaryFile(
|
static Future<File> allocateTemporaryFile(
|
||||||
@@ -43,17 +39,16 @@ class FileService {
|
|||||||
|
|
||||||
static Future<Directory> get temporaryDirectory => getTemporaryDirectory();
|
static Future<Directory> get temporaryDirectory => getTemporaryDirectory();
|
||||||
|
|
||||||
static Future<Directory?> get documentsDirectory async {
|
static Future<Directory> get documentsDirectory async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
return (await getExternalStorageDirectories(
|
return (await getExternalStorageDirectories(
|
||||||
type: StorageDirectory.documents,
|
type: StorageDirectory.documents,
|
||||||
))!
|
))!
|
||||||
.first;
|
.first;
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory()
|
||||||
final dir = Directory('${appDir.path}/documents');
|
.then((dir) => Directory('${dir.path}/documents'));
|
||||||
dir.createSync();
|
return dir.create(recursive: true);
|
||||||
return dir;
|
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedError("Platform not supported.");
|
throw UnsupportedError("Platform not supported.");
|
||||||
}
|
}
|
||||||
@@ -72,34 +67,38 @@ class FileService {
|
|||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
final dir = Directory('${appDir.path}/downloads');
|
final dir = Directory('${appDir.path}/downloads');
|
||||||
dir.createSync();
|
return dir.create(recursive: true);
|
||||||
return dir;
|
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedError("Platform not supported.");
|
throw UnsupportedError("Platform not supported.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Directory?> get scanDirectory async {
|
static Future<Directory> get uploadDirectory async {
|
||||||
if (Platform.isAndroid) {
|
final dir = await getApplicationDocumentsDirectory()
|
||||||
final scanDir = await getExternalStorageDirectories(
|
.then((dir) => Directory('${dir.path}/upload'));
|
||||||
type: StorageDirectory.dcim,
|
return dir.create(recursive: true);
|
||||||
);
|
|
||||||
return scanDir!.first;
|
|
||||||
} else if (Platform.isIOS) {
|
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
|
||||||
final dir = Directory('${appDir.path}/scans');
|
|
||||||
dir.createSync();
|
|
||||||
return dir;
|
|
||||||
} else {
|
|
||||||
throw UnsupportedError("Platform not supported.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> clearUserData() async {
|
static Future<Directory> getConsumptionDirectory(
|
||||||
final scanDir = await scanDirectory;
|
{required String userId}) async {
|
||||||
|
final uploadDir =
|
||||||
|
await uploadDirectory.then((dir) => Directory('${dir.path}/$userId'));
|
||||||
|
return uploadDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Directory> get temporaryScansDirectory async {
|
||||||
final tempDir = await temporaryDirectory;
|
final tempDir = await temporaryDirectory;
|
||||||
await scanDir?.delete(recursive: true);
|
final scansDir = Directory('${tempDir.path}/scans');
|
||||||
|
return scansDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> clearUserData({required String userId}) async {
|
||||||
|
final scanDir = await temporaryScansDirectory;
|
||||||
|
final tempDir = await temporaryDirectory;
|
||||||
|
final consumptionDir = await getConsumptionDirectory(userId: userId);
|
||||||
|
await scanDir.delete(recursive: true);
|
||||||
await tempDir.delete(recursive: true);
|
await tempDir.delete(recursive: true);
|
||||||
|
await consumptionDir.delete(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async {
|
static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async {
|
||||||
@@ -113,11 +112,20 @@ class FileService {
|
|||||||
dir.listSync().map((item) => item.delete(recursive: true)),
|
dir.listSync().map((item) => item.delete(recursive: true)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<File>> getAllFiles(Directory directory) {
|
||||||
|
return directory.list().whereType<File>().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Directory>> getAllSubdirectories(Directory directory) {
|
||||||
|
return directory.list().whereType<Directory>().toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PaperlessDirectoryType {
|
enum PaperlessDirectoryType {
|
||||||
documents,
|
documents,
|
||||||
temporary,
|
temporary,
|
||||||
scans,
|
scans,
|
||||||
download;
|
download,
|
||||||
|
upload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
|
||||||
// import 'package:web_socket_channel/io.dart';
|
|
||||||
|
|
||||||
abstract class StatusService {
|
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
|
||||||
String httpUrl, UserCredentials credentials, String documentFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebSocketStatusService implements StatusService {
|
|
||||||
late WebSocket? socket;
|
|
||||||
// late IOWebSocketChannel? _channel;
|
|
||||||
|
|
||||||
WebSocketStatusService();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
|
||||||
String httpUrl,
|
|
||||||
UserCredentials credentials,
|
|
||||||
String documentFileName,
|
|
||||||
) async {
|
|
||||||
// socket = await WebSocket.connect(
|
|
||||||
// httpUrl.replaceFirst("http", "ws") + "/ws/status/",
|
|
||||||
// customClient: getIt<HttpClient>(),
|
|
||||||
// headers: {
|
|
||||||
// 'Authorization': 'Token ${credentials.token}',
|
|
||||||
// },
|
|
||||||
// ).catchError((_) {
|
|
||||||
// // Use long polling if connection could not be established
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (socket != null) {
|
|
||||||
// socket!.where(isNotNull).listen((event) {
|
|
||||||
// final status = DocumentProcessingStatus.fromJson(event);
|
|
||||||
// getIt<DocumentStatusCubit>().updateStatus(status);
|
|
||||||
// if (status.currentProgress == 100) {
|
|
||||||
// socket!.close();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LongPollingStatusService implements StatusService {
|
|
||||||
final Dio client;
|
|
||||||
const LongPollingStatusService(this.client);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
|
||||||
String httpUrl,
|
|
||||||
UserCredentials credentials,
|
|
||||||
String documentFileName,
|
|
||||||
) async {
|
|
||||||
// final today = DateTime.now();
|
|
||||||
// bool consumptionFinished = false;
|
|
||||||
// int retryCount = 0;
|
|
||||||
|
|
||||||
// getIt<DocumentStatusCubit>().updateStatus(
|
|
||||||
// DocumentProcessingStatus(
|
|
||||||
// currentProgress: 0,
|
|
||||||
// filename: documentFileName,
|
|
||||||
// maxProgress: 100,
|
|
||||||
// message: ProcessingMessage.new_file,
|
|
||||||
// status: ProcessingStatus.working,
|
|
||||||
// taskId: DocumentProcessingStatus.unknownTaskId,
|
|
||||||
// documentId: null,
|
|
||||||
// isApproximated: true,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// do {
|
|
||||||
// final response = await httpClient.get(
|
|
||||||
// Uri.parse(
|
|
||||||
// '$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
|
|
||||||
// );
|
|
||||||
// final data = await compute(
|
|
||||||
// PagedSearchResult.fromJson,
|
|
||||||
// PagedSearchResultJsonSerializer(
|
|
||||||
// jsonDecode(response.body), DocumentModel.fromJson),
|
|
||||||
// );
|
|
||||||
// if (data.count > 0) {
|
|
||||||
// consumptionFinished = true;
|
|
||||||
// final docId = data.results[0].id;
|
|
||||||
// getIt<DocumentStatusCubit>().updateStatus(
|
|
||||||
// DocumentProcessingStatus(
|
|
||||||
// currentProgress: 100,
|
|
||||||
// filename: documentFileName,
|
|
||||||
// maxProgress: 100,
|
|
||||||
// message: ProcessingMessage.finished,
|
|
||||||
// status: ProcessingStatus.success,
|
|
||||||
// taskId: DocumentProcessingStatus.unknownTaskId,
|
|
||||||
// documentId: docId,
|
|
||||||
// isApproximated: true,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// sleep(const Duration(seconds: 1));
|
|
||||||
// } while (!consumptionFinished && retryCount < maxRetries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,24 +54,27 @@ String translateError(BuildContext context, ErrorCode code) {
|
|||||||
ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions,
|
ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions,
|
||||||
ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks,
|
ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks,
|
||||||
ErrorCode.correspondentDeleteFailed =>
|
ErrorCode.correspondentDeleteFailed =>
|
||||||
"Could not delete correspondent, please try again.",
|
S.of(context)!.couldNotDeleteCorrespondent,
|
||||||
ErrorCode.documentTypeDeleteFailed =>
|
ErrorCode.documentTypeDeleteFailed =>
|
||||||
"Could not delete document type, please try again.",
|
S.of(context)!.couldNotDeleteDocumentType,
|
||||||
ErrorCode.tagDeleteFailed => "Could not delete tag, please try again.",
|
ErrorCode.tagDeleteFailed => S.of(context)!.couldNotDeleteTag,
|
||||||
ErrorCode.correspondentUpdateFailed =>
|
|
||||||
"Could not update correspondent, please try again.",
|
|
||||||
ErrorCode.documentTypeUpdateFailed =>
|
|
||||||
"Could not update document type, please try again.",
|
|
||||||
ErrorCode.tagUpdateFailed => "Could not update tag, please try again.",
|
|
||||||
ErrorCode.storagePathDeleteFailed =>
|
ErrorCode.storagePathDeleteFailed =>
|
||||||
"Could not delete storage path, please try again.",
|
S.of(context)!.couldNotDeleteStoragePath,
|
||||||
|
ErrorCode.correspondentUpdateFailed =>
|
||||||
|
S.of(context)!.couldNotUpdateCorrespondent,
|
||||||
|
ErrorCode.documentTypeUpdateFailed =>
|
||||||
|
S.of(context)!.couldNotUpdateDocumentType,
|
||||||
|
ErrorCode.tagUpdateFailed => S.of(context)!.couldNotUpdateTag,
|
||||||
ErrorCode.storagePathUpdateFailed =>
|
ErrorCode.storagePathUpdateFailed =>
|
||||||
"Could not update storage path, please try again.",
|
S.of(context)!.couldNotUpdateStoragePath,
|
||||||
ErrorCode.serverInformationLoadFailed =>
|
ErrorCode.serverInformationLoadFailed =>
|
||||||
"Could not load server information.",
|
S.of(context)!.couldNotLoadServerInformation,
|
||||||
ErrorCode.serverStatisticsLoadFailed => "Could not load server statistics.",
|
ErrorCode.serverStatisticsLoadFailed =>
|
||||||
ErrorCode.uiSettingsLoadFailed => "Could not load UI settings",
|
S.of(context)!.couldNotLoadStatistics,
|
||||||
ErrorCode.loadTasksError => "Could not load tasks.",
|
ErrorCode.uiSettingsLoadFailed => S.of(context)!.couldNotLoadUISettings,
|
||||||
ErrorCode.userNotFound => "User could not be found.",
|
ErrorCode.loadTasksError => S.of(context)!.couldNotLoadTasks,
|
||||||
|
ErrorCode.userNotFound => S.of(context)!.userNotFound,
|
||||||
|
ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView,
|
||||||
|
ErrorCode.userAlreadyExists => S.of(context)!.userAlreadyExists,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
// 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/app_localizations.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)!.reportABug),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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)!.settings),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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)!.aboutThisApp),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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)!.developedBy('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;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/unsaved_changes_warning_dialog.dart';
|
||||||
|
|
||||||
|
class PopWithUnsavedChanges extends StatelessWidget {
|
||||||
|
final bool Function() hasChangesPredicate;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const PopWithUnsavedChanges({
|
||||||
|
super.key,
|
||||||
|
required this.hasChangesPredicate,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
if (hasChangesPredicate()) {
|
||||||
|
final shouldPop = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const UnsavedChangesWarningDialog(),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
return shouldPop;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class UnsavedChangesWarningDialog extends StatelessWidget {
|
||||||
|
const UnsavedChangesWarningDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("Discard changes?"),
|
||||||
|
content: Text(
|
||||||
|
"You have unsaved changes. Do you want to continue without saving? Your changes will be discarded.",
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
DialogCancelButton(),
|
||||||
|
DialogConfirmButton(
|
||||||
|
label: S.of(context)!.continueLabel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
|
|
||||||
class EmptyState extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final String subtitle;
|
|
||||||
final Widget? bottomChild;
|
|
||||||
|
|
||||||
const EmptyState({
|
|
||||||
Key? key,
|
|
||||||
required this.title,
|
|
||||||
required this.subtitle,
|
|
||||||
this.bottomChild,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: size.height / 3,
|
|
||||||
width: size.width / 3,
|
|
||||||
child: SvgPicture.asset("assets/images/empty-state.svg"),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
subtitle,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (bottomChild != null) ...[bottomChild!] else ...[]
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
|
||||||
|
|
||||||
typedef SelectionToTextTransformer<T> = String Function(T suggestion);
|
|
||||||
|
|
||||||
/// Text field that auto-completes user input from a list of items
|
|
||||||
class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
|
||||||
/// Called with the search pattern to get the search suggestions.
|
|
||||||
///
|
|
||||||
/// This callback must not be null. It is be called by the TypeAhead widget
|
|
||||||
/// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html)
|
|
||||||
/// of suggestions either synchronously, or asynchronously (as the result of a
|
|
||||||
/// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)).
|
|
||||||
/// Typically, the list of suggestions should not contain more than 4 or 5
|
|
||||||
/// entries. These entries will then be provided to [itemBuilder] to display
|
|
||||||
/// the suggestions.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// suggestionsCallback: (pattern) async {
|
|
||||||
/// return await _getSuggestions(pattern);
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
final SuggestionsCallback<T> suggestionsCallback;
|
|
||||||
|
|
||||||
/// Called when a suggestion is tapped.
|
|
||||||
///
|
|
||||||
/// This callback must not be null. It is called by the TypeAhead widget and
|
|
||||||
/// provided with the value of the tapped suggestion.
|
|
||||||
///
|
|
||||||
/// For example, you might want to navigate to a specific view when the user
|
|
||||||
/// tabs a suggestion:
|
|
||||||
/// ```dart
|
|
||||||
/// onSuggestionSelected: (suggestion) {
|
|
||||||
/// Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
/// builder: (context) => SearchResult(
|
|
||||||
/// searchItem: suggestion
|
|
||||||
/// )
|
|
||||||
/// ));
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Or to set the value of the text field:
|
|
||||||
/// ```dart
|
|
||||||
/// onSuggestionSelected: (suggestion) {
|
|
||||||
/// _controller.text = suggestion['name'];
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
final SuggestionSelectionCallback<T>? onSuggestionSelected;
|
|
||||||
|
|
||||||
/// Called for each suggestion returned by [suggestionsCallback] to build the
|
|
||||||
/// corresponding widget.
|
|
||||||
///
|
|
||||||
/// This callback must not be null. It is called by the TypeAhead widget for
|
|
||||||
/// each suggestion, and expected to build a widget to display this
|
|
||||||
/// suggestion's info. For example:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// itemBuilder: (context, suggestion) {
|
|
||||||
/// return ListTile(
|
|
||||||
/// title: Text(suggestion['name']),
|
|
||||||
/// subtitle: Text('USD' + suggestion['price'].toString())
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
final ItemBuilder<T> itemBuilder;
|
|
||||||
|
|
||||||
/// The decoration of the material sheet that contains the suggestions.
|
|
||||||
///
|
|
||||||
/// If null, default decoration with an elevation of 4.0 is used
|
|
||||||
final SuggestionsBoxDecoration suggestionsBoxDecoration;
|
|
||||||
|
|
||||||
/// Used to control the `_SuggestionsBox`. Allows manual control to
|
|
||||||
/// open, close, toggle, or resize the `_SuggestionsBox`.
|
|
||||||
final SuggestionsBoxController? suggestionsBoxController;
|
|
||||||
|
|
||||||
/// The duration to wait after the user stops typing before calling
|
|
||||||
/// [suggestionsCallback]
|
|
||||||
///
|
|
||||||
/// This is useful, because, if not set, a request for suggestions will be
|
|
||||||
/// sent for every character that the user types.
|
|
||||||
///
|
|
||||||
/// This duration is set by default to 300 milliseconds
|
|
||||||
final Duration debounceDuration;
|
|
||||||
|
|
||||||
/// Called when waiting for [suggestionsCallback] to return.
|
|
||||||
///
|
|
||||||
/// It is expected to return a widget to display while waiting.
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// (BuildContext context) {
|
|
||||||
/// return Text('Loading...');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If not specified, a [CircularProgressIndicator](https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html) is shown
|
|
||||||
final WidgetBuilder? loadingBuilder;
|
|
||||||
|
|
||||||
/// Called when [suggestionsCallback] returns an empty array.
|
|
||||||
///
|
|
||||||
/// It is expected to return a widget to display when no suggestions are
|
|
||||||
/// available.
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// (BuildContext context) {
|
|
||||||
/// return Text('No Items Found!');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If not specified, a simple text is shown
|
|
||||||
final WidgetBuilder? noItemsFoundBuilder;
|
|
||||||
|
|
||||||
/// Called when [suggestionsCallback] throws an exception.
|
|
||||||
///
|
|
||||||
/// It is called with the error object, and expected to return a widget to
|
|
||||||
/// display when an exception is thrown
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// (BuildContext context, error) {
|
|
||||||
/// return Text('$error');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If not specified, the error is shown in [ThemeData.errorColor](https://docs.flutter.io/flutter/material/ThemeData/errorColor.html)
|
|
||||||
final ErrorBuilder? errorBuilder;
|
|
||||||
|
|
||||||
/// Called to display animations when [suggestionsCallback] returns suggestions
|
|
||||||
///
|
|
||||||
/// It is provided with the suggestions box instance and the animation
|
|
||||||
/// controller, and expected to return some animation that uses the controller
|
|
||||||
/// to display the suggestion box.
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// transitionBuilder: (context, suggestionsBox, animationController) {
|
|
||||||
/// return FadeTransition(
|
|
||||||
/// child: suggestionsBox,
|
|
||||||
/// opacity: CurvedAnimation(
|
|
||||||
/// parent: animationController,
|
|
||||||
/// curve: Curves.fastOutSlowIn
|
|
||||||
/// ),
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// This argument is best used with [animationDuration] and [animationStart]
|
|
||||||
/// to fully control the animation.
|
|
||||||
///
|
|
||||||
/// To fully remove the animation, just return `suggestionsBox`
|
|
||||||
///
|
|
||||||
/// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown.
|
|
||||||
final AnimationTransitionBuilder? transitionBuilder;
|
|
||||||
|
|
||||||
/// The duration that [transitionBuilder] animation takes.
|
|
||||||
///
|
|
||||||
/// This argument is best used with [transitionBuilder] and [animationStart]
|
|
||||||
/// to fully control the animation.
|
|
||||||
///
|
|
||||||
/// Defaults to 500 milliseconds.
|
|
||||||
final Duration animationDuration;
|
|
||||||
|
|
||||||
/// Determine the [SuggestionBox]'s direction.
|
|
||||||
///
|
|
||||||
/// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField]
|
|
||||||
/// and the [_SuggestionsList] will grow **down**.
|
|
||||||
///
|
|
||||||
/// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField]
|
|
||||||
/// and the [_SuggestionsList] will grow **up**.
|
|
||||||
///
|
|
||||||
/// [AxisDirection.left] and [AxisDirection.right] are not allowed.
|
|
||||||
final AxisDirection direction;
|
|
||||||
|
|
||||||
/// The value at which the [transitionBuilder] animation starts.
|
|
||||||
///
|
|
||||||
/// This argument is best used with [transitionBuilder] and [animationDuration]
|
|
||||||
/// to fully control the animation.
|
|
||||||
///
|
|
||||||
/// Defaults to 0.25.
|
|
||||||
final double animationStart;
|
|
||||||
|
|
||||||
/// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
|
|
||||||
/// that the TypeAhead widget displays
|
|
||||||
final TextFieldConfiguration textFieldConfiguration;
|
|
||||||
|
|
||||||
/// How far below the text field should the suggestions box be
|
|
||||||
///
|
|
||||||
/// Defaults to 5.0
|
|
||||||
final double suggestionsBoxVerticalOffset;
|
|
||||||
|
|
||||||
/// If set to true, suggestions will be fetched immediately when the field is
|
|
||||||
/// added to the view.
|
|
||||||
///
|
|
||||||
/// But the suggestions box will only be shown when the field receives focus.
|
|
||||||
/// To make the field receive focus immediately, you can set the `autofocus`
|
|
||||||
/// property in the [textFieldConfiguration] to true
|
|
||||||
///
|
|
||||||
/// Defaults to false
|
|
||||||
final bool getImmediateSuggestions;
|
|
||||||
|
|
||||||
/// If set to true, no loading box will be shown while suggestions are
|
|
||||||
/// being fetched. [loadingBuilder] will also be ignored.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool hideOnLoading;
|
|
||||||
|
|
||||||
/// If set to true, nothing will be shown if there are no results.
|
|
||||||
/// [noItemsFoundBuilder] will also be ignored.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool hideOnEmpty;
|
|
||||||
|
|
||||||
/// If set to true, nothing will be shown if there is an error.
|
|
||||||
/// [errorBuilder] will also be ignored.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool hideOnError;
|
|
||||||
|
|
||||||
/// If set to false, the suggestions box will stay opened after
|
|
||||||
/// the keyboard is closed.
|
|
||||||
///
|
|
||||||
/// Defaults to true.
|
|
||||||
final bool hideSuggestionsOnKeyboardHide;
|
|
||||||
|
|
||||||
/// If set to false, the suggestions box will show a circular
|
|
||||||
/// progress indicator when retrieving suggestions.
|
|
||||||
///
|
|
||||||
/// Defaults to true.
|
|
||||||
final bool keepSuggestionsOnLoading;
|
|
||||||
|
|
||||||
/// If set to true, the suggestions box will remain opened even after
|
|
||||||
/// selecting a suggestion.
|
|
||||||
///
|
|
||||||
/// Note that if this is enabled, the only way
|
|
||||||
/// to close the suggestions box is either manually via the
|
|
||||||
/// `SuggestionsBoxController` or when the user closes the software
|
|
||||||
/// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users
|
|
||||||
/// with a physical keyboard will be unable to close the
|
|
||||||
/// box without a manual way via `SuggestionsBoxController`.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool keepSuggestionsOnSuggestionSelected;
|
|
||||||
|
|
||||||
/// If set to true, in the case where the suggestions box has less than
|
|
||||||
/// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis
|
|
||||||
/// will be temporarily flipped if there's more room available in the opposite
|
|
||||||
/// direction.
|
|
||||||
///
|
|
||||||
/// Defaults to false
|
|
||||||
final bool autoFlipDirection;
|
|
||||||
|
|
||||||
final SelectionToTextTransformer<T>? selectionToTextTransformer;
|
|
||||||
|
|
||||||
/// Controls the text being edited.
|
|
||||||
///
|
|
||||||
/// If null, this widget will create its own [TextEditingController].
|
|
||||||
final TextEditingController? controller;
|
|
||||||
|
|
||||||
final bool hideKeyboard;
|
|
||||||
|
|
||||||
final ScrollController? scrollController;
|
|
||||||
|
|
||||||
/// Creates text field that auto-completes user input from a list of items
|
|
||||||
FormBuilderTypeAhead({
|
|
||||||
Key? key,
|
|
||||||
//From Super
|
|
||||||
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
|
|
||||||
bool enabled = true,
|
|
||||||
FocusNode? focusNode,
|
|
||||||
FormFieldSetter<T>? onSaved,
|
|
||||||
FormFieldValidator<T>? validator,
|
|
||||||
InputDecoration decoration = const InputDecoration(),
|
|
||||||
required String name,
|
|
||||||
required this.itemBuilder,
|
|
||||||
required this.suggestionsCallback,
|
|
||||||
T? initialValue,
|
|
||||||
ValueChanged<T?>? onChanged,
|
|
||||||
ValueTransformer<T?>? valueTransformer,
|
|
||||||
VoidCallback? onReset,
|
|
||||||
this.animationDuration = const Duration(milliseconds: 500),
|
|
||||||
this.animationStart = 0.25,
|
|
||||||
this.autoFlipDirection = false,
|
|
||||||
this.controller,
|
|
||||||
this.debounceDuration = const Duration(milliseconds: 300),
|
|
||||||
this.direction = AxisDirection.down,
|
|
||||||
this.errorBuilder,
|
|
||||||
this.getImmediateSuggestions = false,
|
|
||||||
this.hideKeyboard = false,
|
|
||||||
this.hideOnEmpty = false,
|
|
||||||
this.hideOnError = false,
|
|
||||||
this.hideOnLoading = false,
|
|
||||||
this.hideSuggestionsOnKeyboardHide = true,
|
|
||||||
this.keepSuggestionsOnLoading = true,
|
|
||||||
this.keepSuggestionsOnSuggestionSelected = false,
|
|
||||||
this.loadingBuilder,
|
|
||||||
this.noItemsFoundBuilder,
|
|
||||||
this.onSuggestionSelected,
|
|
||||||
this.scrollController,
|
|
||||||
this.selectionToTextTransformer,
|
|
||||||
this.suggestionsBoxController,
|
|
||||||
this.suggestionsBoxDecoration = const SuggestionsBoxDecoration(),
|
|
||||||
this.suggestionsBoxVerticalOffset = 5.0,
|
|
||||||
this.textFieldConfiguration = const TextFieldConfiguration(),
|
|
||||||
this.transitionBuilder,
|
|
||||||
}) : assert(T == String || selectionToTextTransformer != null),
|
|
||||||
super(
|
|
||||||
key: key,
|
|
||||||
initialValue: initialValue,
|
|
||||||
name: name,
|
|
||||||
validator: validator,
|
|
||||||
valueTransformer: valueTransformer,
|
|
||||||
onChanged: onChanged,
|
|
||||||
autovalidateMode: autovalidateMode,
|
|
||||||
onSaved: onSaved,
|
|
||||||
enabled: enabled,
|
|
||||||
onReset: onReset,
|
|
||||||
decoration: decoration,
|
|
||||||
focusNode: focusNode,
|
|
||||||
builder: (FormFieldState<T?> field) {
|
|
||||||
final state = field as FormBuilderTypeAheadState<T>;
|
|
||||||
final theme = Theme.of(state.context);
|
|
||||||
|
|
||||||
return TypeAheadField<T>(
|
|
||||||
textFieldConfiguration: textFieldConfiguration.copyWith(
|
|
||||||
enabled: state.enabled,
|
|
||||||
controller: state._typeAheadController,
|
|
||||||
style: state.enabled
|
|
||||||
? textFieldConfiguration.style
|
|
||||||
: theme.textTheme.titleMedium!.copyWith(
|
|
||||||
color: theme.disabledColor,
|
|
||||||
),
|
|
||||||
focusNode: state.effectiveFocusNode,
|
|
||||||
decoration: state.decoration,
|
|
||||||
),
|
|
||||||
// TODO HACK to satisfy strictness
|
|
||||||
suggestionsCallback: suggestionsCallback,
|
|
||||||
itemBuilder: itemBuilder,
|
|
||||||
transitionBuilder: (context, suggestionsBox, controller) =>
|
|
||||||
suggestionsBox,
|
|
||||||
onSuggestionSelected: (T suggestion) {
|
|
||||||
state.didChange(suggestion);
|
|
||||||
onSuggestionSelected?.call(suggestion);
|
|
||||||
},
|
|
||||||
getImmediateSuggestions: getImmediateSuggestions,
|
|
||||||
errorBuilder: errorBuilder,
|
|
||||||
noItemsFoundBuilder: noItemsFoundBuilder,
|
|
||||||
loadingBuilder: loadingBuilder,
|
|
||||||
debounceDuration: debounceDuration,
|
|
||||||
suggestionsBoxDecoration: suggestionsBoxDecoration,
|
|
||||||
suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset,
|
|
||||||
animationDuration: animationDuration,
|
|
||||||
animationStart: animationStart,
|
|
||||||
direction: direction,
|
|
||||||
hideOnLoading: hideOnLoading,
|
|
||||||
hideOnEmpty: hideOnEmpty,
|
|
||||||
hideOnError: hideOnError,
|
|
||||||
hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide,
|
|
||||||
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
|
||||||
autoFlipDirection: autoFlipDirection,
|
|
||||||
suggestionsBoxController: suggestionsBoxController,
|
|
||||||
keepSuggestionsOnSuggestionSelected:
|
|
||||||
keepSuggestionsOnSuggestionSelected,
|
|
||||||
hideKeyboard: hideKeyboard,
|
|
||||||
scrollController: scrollController,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class FormBuilderTypeAheadState<T>
|
|
||||||
extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
|
|
||||||
late TextEditingController _typeAheadController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_typeAheadController = widget.controller ??
|
|
||||||
TextEditingController(text: _getTextString(initialValue));
|
|
||||||
// _typeAheadController.addListener(_handleControllerChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
// void _handleControllerChanged() {
|
|
||||||
// Suppress changes that originated from within this class.
|
|
||||||
//
|
|
||||||
// In the case where a controller has been passed in to this widget, we
|
|
||||||
// register this change listener. In these cases, we'll also receive change
|
|
||||||
// notifications for changes originating from within this class -- for
|
|
||||||
// example, the reset() method. In such cases, the FormField value will
|
|
||||||
// already have been set.
|
|
||||||
// if (_typeAheadController.text != value) {
|
|
||||||
// didChange(_typeAheadController.text as T);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChange(T? value) {
|
|
||||||
super.didChange(value);
|
|
||||||
var text = _getTextString(value);
|
|
||||||
|
|
||||||
if (_typeAheadController.text != text) {
|
|
||||||
_typeAheadController.text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
// Dispose the _typeAheadController when initState created it
|
|
||||||
super.dispose();
|
|
||||||
_typeAheadController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void reset() {
|
|
||||||
super.reset();
|
|
||||||
|
|
||||||
_typeAheadController.text = _getTextString(initialValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getTextString(T? value) {
|
|
||||||
var text = value == null
|
|
||||||
? ''
|
|
||||||
: widget.selectionToTextTransformer != null
|
|
||||||
? widget.selectionToTextTransformer!(value)
|
|
||||||
: value.toString();
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FutureOrBuilder<T> extends StatelessWidget {
|
||||||
|
final FutureOr<T>? futureOrValue;
|
||||||
|
|
||||||
|
final T? initialData;
|
||||||
|
|
||||||
|
final AsyncWidgetBuilder<T> builder;
|
||||||
|
|
||||||
|
const FutureOrBuilder({
|
||||||
|
super.key,
|
||||||
|
FutureOr<T>? future,
|
||||||
|
this.initialData,
|
||||||
|
required this.builder,
|
||||||
|
}) : futureOrValue = future;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final futureOrValue = this.futureOrValue;
|
||||||
|
if (futureOrValue is T) {
|
||||||
|
return builder(
|
||||||
|
context,
|
||||||
|
AsyncSnapshot.withData(ConnectionState.done, futureOrValue),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: futureOrValue,
|
||||||
|
initialData: initialData,
|
||||||
|
builder: builder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2019 Simon Lightfoot
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
|
|
||||||
typedef ChipSelected<T> = void Function(T data, bool selected);
|
|
||||||
typedef ChipsBuilder<T> = Widget Function(
|
|
||||||
BuildContext context, ChipsInputState<T> state, T data);
|
|
||||||
|
|
||||||
class ChipsInput<T> extends StatefulWidget {
|
|
||||||
const ChipsInput({
|
|
||||||
super.key,
|
|
||||||
this.decoration = const InputDecoration(),
|
|
||||||
required this.chipBuilder,
|
|
||||||
required this.suggestionBuilder,
|
|
||||||
required this.findSuggestions,
|
|
||||||
required this.onChanged,
|
|
||||||
this.onChipTapped,
|
|
||||||
});
|
|
||||||
|
|
||||||
final InputDecoration decoration;
|
|
||||||
final ChipsInputSuggestions<T> findSuggestions;
|
|
||||||
final ValueChanged<List<T>> onChanged;
|
|
||||||
final ValueChanged<T>? onChipTapped;
|
|
||||||
final ChipsBuilder<T> chipBuilder;
|
|
||||||
final ChipsBuilder<T> suggestionBuilder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ChipsInputState<T> createState() => ChipsInputState<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChipsInputState<T> extends State<ChipsInput<T>> {
|
|
||||||
static const kObjectReplacementChar = 0xFFFC;
|
|
||||||
|
|
||||||
Set<T> _chips = {};
|
|
||||||
List<T> _suggestions = [];
|
|
||||||
int _searchId = 0;
|
|
||||||
|
|
||||||
FocusNode _focusNode = FocusNode();
|
|
||||||
TextEditingValue _value = const TextEditingValue();
|
|
||||||
TextInputConnection? _connection;
|
|
||||||
|
|
||||||
String get text {
|
|
||||||
return String.fromCharCodes(
|
|
||||||
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditingValue get currentTextEditingValue => _value;
|
|
||||||
|
|
||||||
bool get _hasInputConnection =>
|
|
||||||
_connection != null && (_connection?.attached ?? false);
|
|
||||||
|
|
||||||
void requestKeyboard() {
|
|
||||||
if (_focusNode.hasFocus) {
|
|
||||||
_openInputConnection();
|
|
||||||
} else {
|
|
||||||
FocusScope.of(context).requestFocus(_focusNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectSuggestion(T data) {
|
|
||||||
setState(() {
|
|
||||||
_chips.add(data);
|
|
||||||
_updateTextInputState();
|
|
||||||
_suggestions = [];
|
|
||||||
});
|
|
||||||
widget.onChanged(_chips.toList(growable: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
void deleteChip(T data) {
|
|
||||||
setState(() {
|
|
||||||
_chips.remove(data);
|
|
||||||
_updateTextInputState();
|
|
||||||
});
|
|
||||||
widget.onChanged(_chips.toList(growable: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_focusNode = FocusNode();
|
|
||||||
_focusNode.addListener(_onFocusChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onFocusChanged() {
|
|
||||||
if (_focusNode.hasFocus) {
|
|
||||||
_openInputConnection();
|
|
||||||
} else {
|
|
||||||
_closeInputConnectionIfNeeded();
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
// rebuild so that _TextCursor is hidden.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode.dispose();
|
|
||||||
_closeInputConnectionIfNeeded();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openInputConnection() {
|
|
||||||
if (!_hasInputConnection) {
|
|
||||||
_connection?.setEditingState(_value);
|
|
||||||
}
|
|
||||||
_connection?.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeInputConnectionIfNeeded() {
|
|
||||||
if (_hasInputConnection) {
|
|
||||||
_connection?.close();
|
|
||||||
_connection = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var chipsChildren = _chips
|
|
||||||
.map<Widget>(
|
|
||||||
(data) => widget.chipBuilder(context, this, data),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
chipsChildren.add(
|
|
||||||
SizedBox(
|
|
||||||
height: 32.0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_TextCaret(
|
|
||||||
resumed: _focusNode.hasFocus,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
//mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: requestKeyboard,
|
|
||||||
child: InputDecorator(
|
|
||||||
decoration: widget.decoration,
|
|
||||||
isFocused: _focusNode.hasFocus,
|
|
||||||
isEmpty: _value.text.isEmpty,
|
|
||||||
child: Wrap(
|
|
||||||
children: chipsChildren,
|
|
||||||
spacing: 4.0,
|
|
||||||
runSpacing: 4.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: _suggestions.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
return widget.suggestionBuilder(
|
|
||||||
context, this, _suggestions[index]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateEditingValue(TextEditingValue value) {
|
|
||||||
final oldCount = _countReplacements(_value);
|
|
||||||
final newCount = _countReplacements(value);
|
|
||||||
setState(() {
|
|
||||||
if (newCount < oldCount) {
|
|
||||||
_chips = Set.from(_chips.take(newCount));
|
|
||||||
}
|
|
||||||
_value = value;
|
|
||||||
});
|
|
||||||
_onSearchChanged(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
int _countReplacements(TextEditingValue value) {
|
|
||||||
return value.text.codeUnits
|
|
||||||
.where((ch) => ch == kObjectReplacementChar)
|
|
||||||
.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateTextInputState() {
|
|
||||||
final text =
|
|
||||||
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
|
|
||||||
_value = TextEditingValue(
|
|
||||||
text: text,
|
|
||||||
selection: TextSelection.collapsed(offset: text.length),
|
|
||||||
composing: TextRange(start: 0, end: text.length),
|
|
||||||
);
|
|
||||||
_connection?.setEditingState(_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSearchChanged(String value) async {
|
|
||||||
final localId = ++_searchId;
|
|
||||||
final results = await widget.findSuggestions(value);
|
|
||||||
if (_searchId == localId && mounted) {
|
|
||||||
setState(() => _suggestions = results
|
|
||||||
.where((profile) => !_chips.contains(profile))
|
|
||||||
.toList(growable: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TextCaret extends StatefulWidget {
|
|
||||||
const _TextCaret({
|
|
||||||
this.resumed = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool resumed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_TextCursorState createState() => _TextCursorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TextCursorState extends State<_TextCaret>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
bool _displayed = false;
|
|
||||||
late Timer _timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTimer(Timer timer) {
|
|
||||||
setState(() => _displayed = !_displayed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_timer.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return FractionallySizedBox(
|
|
||||||
heightFactor: 0.7,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
|
|
||||||
child: Container(
|
|
||||||
width: 2.0,
|
|
||||||
color: theme.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class AppDrawer extends StatelessWidget {
|
class AppDrawer extends StatelessWidget {
|
||||||
@@ -21,6 +24,7 @@ class AppDrawer extends StatelessWidget {
|
|||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Drawer(
|
child: Drawer(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -60,56 +64,126 @@ class AppDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: Padding(
|
leading: const Icon(Icons.favorite_outline),
|
||||||
padding: const EdgeInsets.only(left: 3),
|
title: Text(S.of(context)!.donate),
|
||||||
child: SvgPicture.asset(
|
onTap: () {
|
||||||
'assets/images/bmc-logo.svg',
|
showDialog(
|
||||||
width: 24,
|
context: context,
|
||||||
height: 24,
|
builder: (context) => AlertDialog(
|
||||||
),
|
icon: const Icon(Icons.favorite),
|
||||||
|
title: Text(S.of(context)!.donate),
|
||||||
|
content: Text(
|
||||||
|
S.of(context)!.donationDialogContent,
|
||||||
|
),
|
||||||
|
actions: const [
|
||||||
|
Text("~ Anton"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: SvgPicture.asset(
|
||||||
|
"assets/images/github-mark.svg",
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
),
|
),
|
||||||
title: Row(
|
title: Text(S.of(context)!.sourceCode),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
trailing: const Icon(
|
||||||
children: [
|
Icons.open_in_new,
|
||||||
Text(S.of(context)!.donateCoffee),
|
size: 16,
|
||||||
const Icon(
|
|
||||||
Icons.open_in_new,
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
"https://www.buymeacoffee.com/astubenbord",
|
"https://github.com/astubenbord/paperless-mobile",
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Consumer<ConsumptionChangeNotifier>(
|
||||||
|
builder: (context, value, child) {
|
||||||
|
final files = value.pendingFiles;
|
||||||
|
final child = ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: const Icon(Icons.drive_folder_upload_outlined),
|
||||||
|
title: const Text("Pending Files"),
|
||||||
|
onTap: () {
|
||||||
|
UploadQueueRoute().push(context);
|
||||||
|
},
|
||||||
|
trailing: Text(
|
||||||
|
'${files.length}',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (files.isEmpty) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return child
|
||||||
|
.animate(onPlay: (c) => c.repeat(reverse: true))
|
||||||
|
.fade(duration: 1.seconds, begin: 1, end: 0.3);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.settings_outlined),
|
leading: const Icon(Icons.settings_outlined),
|
||||||
title: Text(
|
title: Text(
|
||||||
S.of(context)!.settings,
|
S.of(context)!.settings,
|
||||||
),
|
),
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => SettingsRoute().push(context),
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(
|
|
||||||
value: context.read<PaperlessServerStatsApi>()),
|
|
||||||
Provider.value(value: context.read<ApiVersion>()),
|
|
||||||
],
|
|
||||||
child: const SettingsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
|
Text(
|
||||||
|
S.of(context)!.views,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
).padded(16),
|
||||||
|
_buildSavedViews(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSavedViews() {
|
||||||
|
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return state.when(
|
||||||
|
initial: () => const SizedBox.shrink(),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
loaded: (savedViews) {
|
||||||
|
final sidebarViews = savedViews.values
|
||||||
|
.where((element) => element.showInSidebar)
|
||||||
|
.toList();
|
||||||
|
if (sidebarViews.isEmpty) {
|
||||||
|
return Text("Nothing to show here.").paddedOnly(left: 16);
|
||||||
|
}
|
||||||
|
return Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final view = sidebarViews[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(view.name),
|
||||||
|
trailing: Icon(Icons.arrow_forward),
|
||||||
|
onTap: () {
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
context
|
||||||
|
.read<DocumentsCubit>()
|
||||||
|
.updateFilter(filter: view.toDocumentFilter());
|
||||||
|
DocumentsRoute().go(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: sidebarViews.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: () => Text(S.of(context)!.couldNotLoadSavedViews),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _showAboutDialog(BuildContext context) {
|
void _showAboutDialog(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
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/dialog_utils/dialog_cancel_button.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
typedef LabelOptionsSelector<T extends Label> = Map<int, T> Function(
|
|
||||||
DocumentBulkActionState state);
|
|
||||||
|
|
||||||
class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
final String formFieldLabel;
|
|
||||||
final Widget formFieldPrefixIcon;
|
|
||||||
final LabelOptionsSelector<T> availableOptionsSelector;
|
|
||||||
final void Function(int? selectedId) onSubmit;
|
|
||||||
final int? initialValue;
|
|
||||||
final bool canCreateNewLabel;
|
|
||||||
|
|
||||||
const BulkEditLabelBottomSheet({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required this.formFieldLabel,
|
|
||||||
required this.formFieldPrefixIcon,
|
|
||||||
required this.availableOptionsSelector,
|
|
||||||
required this.onSubmit,
|
|
||||||
this.initialValue,
|
|
||||||
required this.canCreateNewLabel,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BulkEditLabelBottomSheet<T>> createState() =>
|
|
||||||
_BulkEditLabelBottomSheetState<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BulkEditLabelBottomSheetState<T extends Label>
|
|
||||||
extends State<BulkEditLabelBottomSheet<T>> {
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.title,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).paddedOnly(bottom: 24),
|
|
||||||
FormBuilder(
|
|
||||||
key: _formKey,
|
|
||||||
child: LabelFormField<T>(
|
|
||||||
initialValue: widget.initialValue != null
|
|
||||||
? IdQueryParameter.fromId(widget.initialValue!)
|
|
||||||
: const IdQueryParameter.unset(),
|
|
||||||
canCreateNewLabel: widget.canCreateNewLabel,
|
|
||||||
name: "labelFormField",
|
|
||||||
options: widget.availableOptionsSelector(state),
|
|
||||||
labelText: widget.formFieldLabel,
|
|
||||||
prefixIcon: widget.formFieldPrefixIcon,
|
|
||||||
allowSelectUnassigned: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
const DialogCancelButton(),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_formKey.currentState?.saveAndValidate() ??
|
|
||||||
false) {
|
|
||||||
final value = _formKey.currentState
|
|
||||||
?.getRawValue('labelFormField')
|
|
||||||
as IdQueryParameter?;
|
|
||||||
widget.onSubmit(value?.maybeWhen(
|
|
||||||
fromId: (id) => id, orElse: () => null));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(S.of(context)!.apply),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
@@ -86,6 +87,7 @@ class _FullscreenBulkEditLabelPageState<T extends Label>
|
|||||||
selectionCount: _labels.length,
|
selectionCount: _labels.length,
|
||||||
floatingActionButton: !hideFab
|
floatingActionButton: !hideFab
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
|
heroTag: "fab_fullscreen_bulk_edit_label",
|
||||||
onPressed: _onSubmit,
|
onPressed: _onSubmit,
|
||||||
label: Text(S.of(context)!.apply),
|
label: Text(S.of(context)!.apply),
|
||||||
icon: const Icon(Icons.done),
|
icon: const Icon(Icons.done),
|
||||||
@@ -122,7 +124,7 @@ class _FullscreenBulkEditLabelPageState<T extends Label>
|
|||||||
|
|
||||||
void _onSubmit() async {
|
void _onSubmit() async {
|
||||||
if (_selection == null) {
|
if (_selection == null) {
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
} else {
|
} else {
|
||||||
bool shouldPerformAction;
|
bool shouldPerformAction;
|
||||||
if (_selection!.label == null) {
|
if (_selection!.label == null) {
|
||||||
@@ -148,7 +150,7 @@ class _FullscreenBulkEditLabelPageState<T extends Label>
|
|||||||
}
|
}
|
||||||
if (shouldPerformAction) {
|
if (shouldPerformAction) {
|
||||||
widget.onSubmit(_selection!.label);
|
widget.onSubmit(_selection!.label);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,7 @@
|
|||||||
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:go_router/go_router.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
@@ -74,6 +75,7 @@ class _FullscreenBulkEditTagsWidgetState
|
|||||||
controller: _controller,
|
controller: _controller,
|
||||||
floatingActionButton: _addTags.isNotEmpty || _removeTags.isNotEmpty
|
floatingActionButton: _addTags.isNotEmpty || _removeTags.isNotEmpty
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
|
heroTag: "fab_fullscreen_bulk_edit_tags",
|
||||||
label: Text(S.of(context)!.apply),
|
label: Text(S.of(context)!.apply),
|
||||||
icon: const Icon(Icons.done),
|
icon: const Icon(Icons.done),
|
||||||
onPressed: _submit,
|
onPressed: _submit,
|
||||||
@@ -173,7 +175,7 @@ class _FullscreenBulkEditTagsWidgetState
|
|||||||
removeTagIds: _removeTags,
|
removeTagIds: _removeTags,
|
||||||
addTagIds: _addTags,
|
addTagIds: _addTags,
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
// import 'package:flutter/material.dart';
|
|
||||||
// import 'package:flutter/src/widgets/framework.dart';
|
|
||||||
// import 'package:flutter/src/widgets/placeholder.dart';
|
|
||||||
|
|
||||||
// class LabelBulkSelectionWidget extends StatelessWidget {
|
|
||||||
// final int labelId;
|
|
||||||
// final String title;
|
|
||||||
// final bool selected;
|
|
||||||
// final bool excluded;
|
|
||||||
// final Widget Function(int id) leadingWidgetBuilder;
|
|
||||||
// final void Function(int id) onSelected;
|
|
||||||
// final void Function(int id) onUnselected;
|
|
||||||
// final void Function(int id) onRemoved;
|
|
||||||
|
|
||||||
// const LabelBulkSelectionWidget({
|
|
||||||
// super.key,
|
|
||||||
// required this.labelId,
|
|
||||||
// required this.title,
|
|
||||||
// required this.leadingWidgetBuilder,
|
|
||||||
// required this.onSelected,
|
|
||||||
// required this.onUnselected,
|
|
||||||
// required this.onRemoved,
|
|
||||||
// });
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return ListTile(
|
|
||||||
// title: Text(title),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -8,13 +8,12 @@ import 'package:open_filex/open_filex.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_description.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.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:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
part 'document_details_cubit.freezed.dart';
|
part 'document_details_cubit.freezed.dart';
|
||||||
part 'document_details_state.dart';
|
part 'document_details_state.dart';
|
||||||
|
|
||||||
@@ -45,7 +44,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
loadSuggestions();
|
|
||||||
loadMetaData();
|
loadMetaData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,13 +52,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
_notifier.notifyDeleted(document);
|
_notifier.notifyDeleted(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadSuggestions() async {
|
|
||||||
final suggestions = await _api.findSuggestions(state.document);
|
|
||||||
if (!isClosed) {
|
|
||||||
emit(state.copyWith(suggestions: suggestions));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadMetaData() async {
|
Future<void> loadMetaData() async {
|
||||||
final metaData = await _api.getMetaData(state.document);
|
final metaData = await _api.getMetaData(state.document);
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
@@ -101,11 +92,9 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
if (state.metaData == null) {
|
if (state.metaData == null) {
|
||||||
await loadMetaData();
|
await loadMetaData();
|
||||||
}
|
}
|
||||||
final desc = FileDescription.fromPath(
|
final filePath = state.metaData!.mediaFilename.replaceAll("/", " ");
|
||||||
state.metaData!.mediaFilename.replaceAll("/", " "),
|
|
||||||
);
|
|
||||||
|
|
||||||
final fileName = "${desc.filename}.pdf";
|
final fileName = "${p.basenameWithoutExtension(filePath)}.pdf";
|
||||||
final file = File("${cacheDir.path}/$fileName");
|
final file = File("${cacheDir.path}/$fileName");
|
||||||
|
|
||||||
if (!file.existsSync()) {
|
if (!file.existsSync()) {
|
||||||
@@ -128,51 +117,63 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
Future<void> downloadDocument({
|
Future<void> downloadDocument({
|
||||||
bool downloadOriginal = false,
|
bool downloadOriginal = false,
|
||||||
required String locale,
|
required String locale,
|
||||||
|
required String userId,
|
||||||
}) async {
|
}) async {
|
||||||
if (state.metaData == null) {
|
if (state.metaData == null) {
|
||||||
await loadMetaData();
|
await loadMetaData();
|
||||||
}
|
}
|
||||||
String filePath = _buildDownloadFilePath(
|
String targetPath = _buildDownloadFilePath(
|
||||||
downloadOriginal,
|
downloadOriginal,
|
||||||
await FileService.downloadsDirectory,
|
await FileService.downloadsDirectory,
|
||||||
);
|
);
|
||||||
final desc = FileDescription.fromPath(
|
|
||||||
state.metaData!.mediaFilename
|
if (!await File(targetPath).exists()) {
|
||||||
.replaceAll("/", " "), // Flatten directory structure
|
await File(targetPath).create();
|
||||||
);
|
|
||||||
if (!File(filePath).existsSync()) {
|
|
||||||
File(filePath).createSync();
|
|
||||||
} else {
|
} else {
|
||||||
return _notificationService.notifyFileDownload(
|
await _notificationService.notifyFileDownload(
|
||||||
document: state.document,
|
document: state.document,
|
||||||
filename: "${desc.filename}.${desc.extension}",
|
filename: p.basename(targetPath),
|
||||||
filePath: filePath,
|
filePath: targetPath,
|
||||||
finished: true,
|
finished: true,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
|
userId: userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _notificationService.notifyFileDownload(
|
// await _notificationService.notifyFileDownload(
|
||||||
document: state.document,
|
// document: state.document,
|
||||||
filename: "${desc.filename}.${desc.extension}",
|
// filename: p.basename(targetPath),
|
||||||
filePath: filePath,
|
// filePath: targetPath,
|
||||||
finished: false,
|
// finished: false,
|
||||||
locale: locale,
|
// locale: locale,
|
||||||
);
|
// userId: userId,
|
||||||
|
// );
|
||||||
|
|
||||||
await _api.downloadToFile(
|
await _api.downloadToFile(
|
||||||
state.document,
|
state.document,
|
||||||
filePath,
|
targetPath,
|
||||||
original: downloadOriginal,
|
original: downloadOriginal,
|
||||||
|
onProgressChanged: (progress) {
|
||||||
|
_notificationService.notifyFileDownload(
|
||||||
|
document: state.document,
|
||||||
|
filename: p.basename(targetPath),
|
||||||
|
filePath: targetPath,
|
||||||
|
finished: true,
|
||||||
|
locale: locale,
|
||||||
|
userId: userId,
|
||||||
|
progress: progress,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await _notificationService.notifyFileDownload(
|
await _notificationService.notifyFileDownload(
|
||||||
document: state.document,
|
document: state.document,
|
||||||
filename: "${desc.filename}.${desc.extension}",
|
filename: p.basename(targetPath),
|
||||||
filePath: filePath,
|
filePath: targetPath,
|
||||||
finished: true,
|
finished: true,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
|
userId: userId,
|
||||||
);
|
);
|
||||||
debugPrint("Downloaded file to $filePath");
|
debugPrint("Downloaded file to $targetPath");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> shareDocument({bool shareOriginal = false}) async {
|
Future<void> shareDocument({bool shareOriginal = false}) async {
|
||||||
@@ -223,12 +224,9 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _buildDownloadFilePath(bool original, Directory dir) {
|
String _buildDownloadFilePath(bool original, Directory dir) {
|
||||||
final description = FileDescription.fromPath(
|
final normalizedPath = state.metaData!.mediaFilename.replaceAll("/", " ");
|
||||||
state.metaData!.mediaFilename
|
final extension = original ? p.extension(normalizedPath) : '.pdf';
|
||||||
.replaceAll("/", " "), // Flatten directory structure
|
return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension";
|
||||||
);
|
|
||||||
final extension = original ? description.extension : 'pdf';
|
|
||||||
return "${dir.path}/${description.filename}.$extension";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ class DocumentDetailsState with _$DocumentDetailsState {
|
|||||||
DocumentMetaData? metaData,
|
DocumentMetaData? metaData,
|
||||||
@Default(false) bool isFullContentLoaded,
|
@Default(false) bool isFullContentLoaded,
|
||||||
String? fullContent,
|
String? fullContent,
|
||||||
FieldSuggestions? suggestions,
|
|
||||||
@Default({}) Map<int, Correspondent> correspondents,
|
@Default({}) Map<int, Correspondent> correspondents,
|
||||||
@Default({}) Map<int, DocumentType> documentTypes,
|
@Default({}) Map<int, DocumentType> documentTypes,
|
||||||
@Default({}) Map<int, Tag> tags,
|
@Default({}) Map<int, Tag> tags,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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:go_router/go_router.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';
|
||||||
@@ -14,16 +15,14 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document
|
|||||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
|
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
|
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
|
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
|
||||||
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
|
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
|
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
|
||||||
class DocumentDetailsPage extends StatefulWidget {
|
class DocumentDetailsPage extends StatefulWidget {
|
||||||
final bool isLabelClickable;
|
final bool isLabelClickable;
|
||||||
@@ -46,9 +45,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final apiVersion = context.watch<ApiVersion>();
|
final hasMultiUserSupport =
|
||||||
|
context.watch<LocalUserAccount>().hasMultiUserSupport;
|
||||||
final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0);
|
final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0);
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
@@ -86,45 +85,52 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
collapsedHeight: kToolbarHeight,
|
collapsedHeight: kToolbarHeight,
|
||||||
expandedHeight: 250.0,
|
expandedHeight: 250.0,
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
background: Stack(
|
background: BlocBuilder<DocumentDetailsCubit,
|
||||||
alignment: Alignment.topCenter,
|
DocumentDetailsState>(
|
||||||
children: [
|
builder: (context, state) {
|
||||||
BlocBuilder<DocumentDetailsCubit,
|
return Hero(
|
||||||
DocumentDetailsState>(
|
tag: "thumb_${state.document.id}",
|
||||||
builder: (context, state) {
|
child: GestureDetector(
|
||||||
return Positioned.fill(
|
onTap: () {
|
||||||
child: DocumentPreview(
|
DocumentPreviewRoute($extra: state.document)
|
||||||
document: state.document,
|
.push(context);
|
||||||
fit: BoxFit.cover,
|
},
|
||||||
),
|
child: Stack(
|
||||||
);
|
alignment: Alignment.topCenter,
|
||||||
},
|
children: [
|
||||||
),
|
Positioned.fill(
|
||||||
Positioned.fill(
|
child: DocumentPreview(
|
||||||
top: 0,
|
enableHero: false,
|
||||||
child: DecoratedBox(
|
document: state.document,
|
||||||
decoration: BoxDecoration(
|
fit: BoxFit.cover,
|
||||||
gradient: LinearGradient(
|
),
|
||||||
colors: [
|
),
|
||||||
Theme.of(context)
|
Positioned.fill(
|
||||||
.colorScheme
|
child: DecoratedBox(
|
||||||
.background
|
decoration: BoxDecoration(
|
||||||
.withOpacity(0.8),
|
gradient: LinearGradient(
|
||||||
Theme.of(context)
|
stops: [0.2, 0.4],
|
||||||
.colorScheme
|
colors: [
|
||||||
.background
|
Theme.of(context)
|
||||||
.withOpacity(0.5),
|
.colorScheme
|
||||||
Colors.transparent,
|
.background
|
||||||
Colors.transparent,
|
.withOpacity(0.6),
|
||||||
Colors.transparent,
|
Theme.of(context)
|
||||||
],
|
.colorScheme
|
||||||
begin: Alignment.topCenter,
|
.background
|
||||||
end: Alignment.bottomCenter,
|
.withOpacity(0.3),
|
||||||
),
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottom: ColoredTabBar(
|
bottom: ColoredTabBar(
|
||||||
@@ -171,7 +177,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (apiVersion.hasMultiUserSupport)
|
if (hasMultiUserSupport && false)
|
||||||
Tab(
|
Tab(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Permissions",
|
"Permissions",
|
||||||
@@ -195,6 +201,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
documentId: state.document.id,
|
documentId: state.document.id,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -259,7 +266,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (apiVersion.hasMultiUserSupport)
|
if (hasMultiUserSupport && false)
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
controller: _pagingScrollController,
|
controller: _pagingScrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -286,8 +293,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEditButton() {
|
Widget _buildEditButton() {
|
||||||
|
final currentUser = context.watch<LocalUserAccount>();
|
||||||
|
|
||||||
bool canEdit = context.watchInternetConnection &&
|
bool canEdit = context.watchInternetConnection &&
|
||||||
LocalUserAccount.current.paperlessUser.canEditDocuments;
|
currentUser.paperlessUser.canEditDocuments;
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -301,8 +310,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
preferBelow: false,
|
preferBelow: false,
|
||||||
verticalOffset: 40,
|
verticalOffset: 40,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
|
heroTag: "fab_document_details",
|
||||||
child: const Icon(Icons.edit),
|
child: const Icon(Icons.edit),
|
||||||
onPressed: () => _onEdit(state.document),
|
onPressed: () => EditDocumentRoute(state.document).push(context),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -315,38 +325,48 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
return BottomAppBar(
|
return BottomAppBar(
|
||||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
final isConnected = connectivityState.isConnected;
|
final currentUser = context.watch<LocalUserAccount>();
|
||||||
|
|
||||||
final canDelete = isConnected &&
|
|
||||||
LocalUserAccount.current.paperlessUser.canDeleteDocuments;
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
ConnectivityAwareActionWrapper(
|
||||||
tooltip: S.of(context)!.deleteDocumentTooltip,
|
disabled: !currentUser.paperlessUser.canDeleteDocuments,
|
||||||
icon: const Icon(Icons.delete),
|
offlineBuilder: (context, child) {
|
||||||
onPressed:
|
return const IconButton(
|
||||||
canDelete ? () => _onDelete(state.document) : null,
|
icon: Icon(Icons.delete),
|
||||||
).paddedSymmetrically(horizontal: 4),
|
onPressed: null,
|
||||||
DocumentDownloadButton(
|
).paddedSymmetrically(horizontal: 4);
|
||||||
document: state.document,
|
},
|
||||||
enabled: isConnected,
|
child: IconButton(
|
||||||
|
tooltip: S.of(context)!.deleteDocumentTooltip,
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () => _onDelete(state.document),
|
||||||
|
).paddedSymmetrically(horizontal: 4),
|
||||||
|
),
|
||||||
|
ConnectivityAwareActionWrapper(
|
||||||
|
offlineBuilder: (context, child) =>
|
||||||
|
const DocumentDownloadButton(
|
||||||
|
document: null,
|
||||||
|
enabled: false,
|
||||||
|
),
|
||||||
|
child: DocumentDownloadButton(
|
||||||
|
document: state.document,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConnectivityAwareActionWrapper(
|
||||||
|
offlineBuilder: (context, child) => const IconButton(
|
||||||
|
icon: Icon(Icons.open_in_new),
|
||||||
|
onPressed: null,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: S.of(context)!.openInSystemViewer,
|
||||||
|
icon: const Icon(Icons.open_in_new),
|
||||||
|
onPressed: _onOpenFileInSystemViewer,
|
||||||
|
).paddedOnly(right: 4.0),
|
||||||
),
|
),
|
||||||
//TODO: Enable again, need new pdf viewer package...
|
|
||||||
IconButton(
|
|
||||||
tooltip: S.of(context)!.previewTooltip,
|
|
||||||
icon: const Icon(Icons.visibility),
|
|
||||||
onPressed:
|
|
||||||
(isConnected) ? () => _onOpen(state.document) : null,
|
|
||||||
).paddedOnly(right: 4.0),
|
|
||||||
IconButton(
|
|
||||||
tooltip: S.of(context)!.openInSystemViewer,
|
|
||||||
icon: const Icon(Icons.open_in_new),
|
|
||||||
onPressed: isConnected ? _onOpenFileInSystemViewer : null,
|
|
||||||
).paddedOnly(right: 4.0),
|
|
||||||
DocumentShareButton(document: state.document),
|
DocumentShareButton(document: state.document),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: S.of(context)!.print, //TODO: INTL
|
tooltip: S.of(context)!.print,
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
context.read<DocumentDetailsCubit>().printDocument(),
|
context.read<DocumentDetailsCubit>().printDocument(),
|
||||||
icon: const Icon(Icons.print),
|
icon: const Icon(Icons.print),
|
||||||
@@ -360,47 +380,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onEdit(DocumentModel document) async {
|
|
||||||
{
|
|
||||||
final cubit = context.read<DocumentDetailsCubit>();
|
|
||||||
Navigator.push<bool>(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiBlocProvider(
|
|
||||||
providers: [
|
|
||||||
BlocProvider.value(
|
|
||||||
value: DocumentEditCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
document: document,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider<DocumentDetailsCubit>.value(
|
|
||||||
value: cubit,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: BlocListener<DocumentEditCubit, DocumentEditState>(
|
|
||||||
listenWhen: (previous, current) =>
|
|
||||||
previous.document != current.document,
|
|
||||||
listener: (context, state) {
|
|
||||||
cubit.replace(state.document);
|
|
||||||
},
|
|
||||||
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return DocumentEditPage(
|
|
||||||
suggestions: state.suggestions,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
maintainState: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onOpenFileInSystemViewer() async {
|
void _onOpenFileInSystemViewer() async {
|
||||||
final status =
|
final status =
|
||||||
await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
|
await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
|
||||||
@@ -427,25 +406,21 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
if (delete) {
|
if (delete) {
|
||||||
try {
|
try {
|
||||||
await context.read<DocumentDetailsCubit>().delete(document);
|
await context.read<DocumentDetailsCubit>().delete(document);
|
||||||
showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
|
// showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} finally {
|
} finally {
|
||||||
// Document deleted => go back to primary route
|
do {
|
||||||
Navigator.popUntil(context, (route) => route.isFirst);
|
context.pop();
|
||||||
|
} while (context.canPop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onOpen(DocumentModel document) async {
|
Future<void> _onOpen(DocumentModel document) async {
|
||||||
Navigator.of(context).push(
|
DocumentPreviewRoute(
|
||||||
MaterialPageRoute(
|
$extra: document,
|
||||||
builder: (_) => DocumentView(
|
title: document.title,
|
||||||
documentBytes:
|
).push(context);
|
||||||
context.read<PaperlessDocumentsApi>().download(document),
|
|
||||||
title: document.title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userCanEditDocument =
|
final userCanEditDocument =
|
||||||
LocalUserAccount.current.paperlessUser.canEditDocuments;
|
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
|
||||||
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
|
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous.document.archiveSerialNumber !=
|
previous.document.archiveSerialNumber !=
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||||
@@ -90,9 +91,11 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isDownloadPending = true);
|
setState(() => _isDownloadPending = true);
|
||||||
|
final userId = context.read<LocalUserAccount>().id;
|
||||||
await context.read<DocumentDetailsCubit>().downloadDocument(
|
await context.read<DocumentDetailsCubit>().downloadDocument(
|
||||||
downloadOriginal: original,
|
downloadOriginal: original,
|
||||||
locale: globalSettings.preferredLocaleSubtag,
|
locale: globalSettings.preferredLocaleSubtag,
|
||||||
|
userId: userId,
|
||||||
);
|
);
|
||||||
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package: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/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart';
|
import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart';
|
||||||
@@ -25,6 +26,7 @@ class DocumentMetaDataWidget extends StatefulWidget {
|
|||||||
class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
|
class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||||
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.metaData == null) {
|
if (state.metaData == null) {
|
||||||
@@ -37,9 +39,10 @@ class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
|
|||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate(
|
||||||
[
|
[
|
||||||
ArchiveSerialNumberField(
|
if (currentUser.canEditDocuments)
|
||||||
document: widget.document,
|
ArchiveSerialNumberField(
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
document: widget.document,
|
||||||
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
DetailsItem.text(
|
DetailsItem.text(
|
||||||
DateFormat().format(widget.document.modified),
|
DateFormat().format(widget.document.modified),
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.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/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
@@ -30,62 +31,66 @@ class DocumentOverviewWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverList(
|
return SliverList.list(
|
||||||
delegate: SliverChildListDelegate(
|
children: [
|
||||||
[
|
DetailsItem(
|
||||||
|
label: S.of(context)!.title,
|
||||||
|
content: HighlightedText(
|
||||||
|
text: document.title,
|
||||||
|
highlights: queryString?.split(" ") ?? [],
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
).paddedOnly(bottom: itemSpacing),
|
||||||
|
DetailsItem.text(
|
||||||
|
DateFormat.yMMMMd().format(document.created),
|
||||||
|
context: context,
|
||||||
|
label: S.of(context)!.createdAt,
|
||||||
|
).paddedOnly(bottom: itemSpacing),
|
||||||
|
if (document.documentType != null &&
|
||||||
|
context
|
||||||
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canViewDocumentTypes)
|
||||||
DetailsItem(
|
DetailsItem(
|
||||||
label: S.of(context)!.title,
|
label: S.of(context)!.documentType,
|
||||||
content: HighlightedText(
|
content: LabelText<DocumentType>(
|
||||||
text: document.title,
|
|
||||||
highlights: queryString?.split(" ") ?? [],
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
label: availableDocumentTypes[document.documentType],
|
||||||
),
|
),
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
DetailsItem.text(
|
if (document.correspondent != null &&
|
||||||
DateFormat.yMMMMd().format(document.created),
|
context
|
||||||
context: context,
|
.watch<LocalUserAccount>()
|
||||||
label: S.of(context)!.createdAt,
|
.paperlessUser
|
||||||
|
.canViewCorrespondents)
|
||||||
|
DetailsItem(
|
||||||
|
label: S.of(context)!.correspondent,
|
||||||
|
content: LabelText<Correspondent>(
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
label: availableCorrespondents[document.correspondent],
|
||||||
|
),
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
if (document.documentType != null &&
|
if (document.storagePath != null &&
|
||||||
LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
|
context.watch<LocalUserAccount>().paperlessUser.canViewStoragePaths)
|
||||||
DetailsItem(
|
DetailsItem(
|
||||||
label: S.of(context)!.documentType,
|
label: S.of(context)!.storagePath,
|
||||||
content: LabelText<DocumentType>(
|
content: LabelText<StoragePath>(
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
label: availableStoragePaths[document.storagePath],
|
||||||
label: availableDocumentTypes[document.documentType],
|
),
|
||||||
|
).paddedOnly(bottom: itemSpacing),
|
||||||
|
if (document.tags.isNotEmpty &&
|
||||||
|
context.watch<LocalUserAccount>().paperlessUser.canViewTags)
|
||||||
|
DetailsItem(
|
||||||
|
label: S.of(context)!.tags,
|
||||||
|
content: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: TagsWidget(
|
||||||
|
isClickable: false,
|
||||||
|
tags: document.tags.map((e) => availableTags[e]!).toList(),
|
||||||
),
|
),
|
||||||
).paddedOnly(bottom: itemSpacing),
|
),
|
||||||
if (document.correspondent != null &&
|
).paddedOnly(bottom: itemSpacing),
|
||||||
LocalUserAccount.current.paperlessUser.canViewCorrespondents)
|
],
|
||||||
DetailsItem(
|
|
||||||
label: S.of(context)!.correspondent,
|
|
||||||
content: LabelText<Correspondent>(
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
label: availableCorrespondents[document.correspondent],
|
|
||||||
),
|
|
||||||
).paddedOnly(bottom: itemSpacing),
|
|
||||||
if (document.storagePath != null &&
|
|
||||||
LocalUserAccount.current.paperlessUser.canViewStoragePaths)
|
|
||||||
DetailsItem(
|
|
||||||
label: S.of(context)!.storagePath,
|
|
||||||
content: LabelText<StoragePath>(
|
|
||||||
label: availableStoragePaths[document.storagePath],
|
|
||||||
),
|
|
||||||
).paddedOnly(bottom: itemSpacing),
|
|
||||||
if (document.tags.isNotEmpty &&
|
|
||||||
LocalUserAccount.current.paperlessUser.canViewTags)
|
|
||||||
DetailsItem(
|
|
||||||
label: S.of(context)!.tags,
|
|
||||||
content: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: TagsWidget(
|
|
||||||
isClickable: false,
|
|
||||||
tags: document.tags.map((e) => availableTags[e]!).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddedOnly(bottom: itemSpacing),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:paperless_mobile/features/document_details/cubit/document_detail
|
|||||||
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/file_download_type.dart';
|
import 'package:paperless_mobile/features/settings/model/file_download_type.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
@@ -34,19 +35,25 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return ConnectivityAwareActionWrapper(
|
||||||
tooltip: S.of(context)!.shareTooltip,
|
offlineBuilder: (context, child) => const IconButton(
|
||||||
icon: _isDownloadPending
|
icon: Icon(Icons.share),
|
||||||
? const SizedBox(
|
onPressed: null,
|
||||||
height: 16,
|
),
|
||||||
width: 16,
|
child: IconButton(
|
||||||
child: CircularProgressIndicator(),
|
tooltip: S.of(context)!.shareTooltip,
|
||||||
)
|
icon: _isDownloadPending
|
||||||
: const Icon(Icons.share),
|
? const SizedBox(
|
||||||
onPressed: widget.document != null && widget.enabled
|
height: 16,
|
||||||
? () => _onShare(widget.document!)
|
width: 16,
|
||||||
: null,
|
child: CircularProgressIndicator(),
|
||||||
).paddedOnly(right: 4);
|
)
|
||||||
|
: const Icon(Icons.share),
|
||||||
|
onPressed: widget.document != null && widget.enabled
|
||||||
|
? () => _onShare(widget.document!)
|
||||||
|
: null,
|
||||||
|
).paddedOnly(right: 4),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onShare(DocumentModel document) async {
|
Future<void> _onShare(DocumentModel document) async {
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadFieldSuggestions() async {
|
||||||
|
final suggestions = await _docsApi.findSuggestions(state.document);
|
||||||
|
emit(state.copyWith(suggestions: suggestions));
|
||||||
|
}
|
||||||
|
|
||||||
void replace(DocumentModel document) {
|
void replace(DocumentModel document) {
|
||||||
emit(state.copyWith(document: document));
|
emit(state.copyWith(document: document));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ part of 'document_edit_cubit.dart';
|
|||||||
class DocumentEditState with _$DocumentEditState {
|
class DocumentEditState with _$DocumentEditState {
|
||||||
const factory DocumentEditState({
|
const factory DocumentEditState({
|
||||||
required DocumentModel document,
|
required DocumentModel document,
|
||||||
|
FieldSuggestions? suggestions,
|
||||||
@Default({}) Map<int, Correspondent> correspondents,
|
@Default({}) Map<int, Correspondent> correspondents,
|
||||||
@Default({}) Map<int, DocumentType> documentTypes,
|
@Default({}) Map<int, DocumentType> documentTypes,
|
||||||
@Default({}) Map<int, StoragePath> storagePaths,
|
@Default({}) Map<int, StoragePath> storagePaths,
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
|
import 'package:go_router/go_router.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/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart';
|
||||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
|
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
|
||||||
@@ -18,14 +19,11 @@ 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/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class DocumentEditPage extends StatefulWidget {
|
class DocumentEditPage extends StatefulWidget {
|
||||||
final FieldSuggestions? suggestions;
|
|
||||||
const DocumentEditPage({
|
const DocumentEditPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.suggestions,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,256 +40,261 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
static const fkContent = 'content';
|
static const fkContent = 'content';
|
||||||
|
|
||||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||||
bool _isSubmitLoading = false;
|
|
||||||
|
|
||||||
late final FieldSuggestions? _filteredSuggestions;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_filteredSuggestions = widget.suggestions
|
|
||||||
?.documentDifference(context.read<DocumentEditCubit>().state.document);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||||
builder: (context, state) {
|
return PopWithUnsavedChanges(
|
||||||
return DefaultTabController(
|
hasChangesPredicate: () => _formKey.currentState?.isDirty ?? false,
|
||||||
length: 2,
|
child: BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
||||||
child: Scaffold(
|
builder: (context, state) {
|
||||||
resizeToAvoidBottomInset: false,
|
final filteredSuggestions = state.suggestions?.documentDifference(
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
context.read<DocumentEditCubit>().state.document);
|
||||||
onPressed: () => _onSubmit(state.document),
|
return DefaultTabController(
|
||||||
icon: const Icon(Icons.save),
|
length: 2,
|
||||||
label: Text(S.of(context)!.saveChanges),
|
child: Scaffold(
|
||||||
),
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: AppBar(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
title: Text(S.of(context)!.editDocument),
|
heroTag: "fab_document_edit",
|
||||||
bottom: TabBar(
|
onPressed: () => _onSubmit(state.document),
|
||||||
tabs: [
|
icon: const Icon(Icons.save),
|
||||||
Tab(
|
label: Text(S.of(context)!.saveChanges),
|
||||||
text: S.of(context)!.overview,
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
text: S.of(context)!.content,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
appBar: AppBar(
|
||||||
extendBody: true,
|
title: Text(S.of(context)!.editDocument),
|
||||||
body: Padding(
|
bottom: TabBar(
|
||||||
padding: EdgeInsets.only(
|
tabs: [
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
Tab(text: S.of(context)!.overview),
|
||||||
top: 8,
|
Tab(text: S.of(context)!.content)
|
||||||
left: 8,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: FormBuilder(
|
|
||||||
key: _formKey,
|
|
||||||
child: TabBarView(
|
|
||||||
children: [
|
|
||||||
ListView(
|
|
||||||
children: [
|
|
||||||
_buildTitleFormField(state.document.title).padded(),
|
|
||||||
_buildCreatedAtFormField(state.document.created)
|
|
||||||
.padded(),
|
|
||||||
// Correspondent form field
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
LabelFormField<Correspondent>(
|
|
||||||
showAnyAssignedOption: false,
|
|
||||||
showNotAssignedOption: false,
|
|
||||||
addLabelPageBuilder: (initialValue) =>
|
|
||||||
RepositoryProvider.value(
|
|
||||||
value: context.read<LabelRepository>(),
|
|
||||||
child: AddCorrespondentPage(
|
|
||||||
initialName: initialValue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
addLabelText: S.of(context)!.addCorrespondent,
|
|
||||||
labelText: S.of(context)!.correspondent,
|
|
||||||
options: context
|
|
||||||
.watch<DocumentEditCubit>()
|
|
||||||
.state
|
|
||||||
.correspondents,
|
|
||||||
initialValue:
|
|
||||||
state.document.correspondent != null
|
|
||||||
? IdQueryParameter.fromId(
|
|
||||||
state.document.correspondent!)
|
|
||||||
: const IdQueryParameter.unset(),
|
|
||||||
name: fkCorrespondent,
|
|
||||||
prefixIcon: const Icon(Icons.person_outlined),
|
|
||||||
allowSelectUnassigned: true,
|
|
||||||
canCreateNewLabel: LocalUserAccount.current
|
|
||||||
.paperlessUser.canCreateCorrespondents,
|
|
||||||
),
|
|
||||||
if (_filteredSuggestions
|
|
||||||
?.hasSuggestedCorrespondents ??
|
|
||||||
false)
|
|
||||||
_buildSuggestionsSkeleton<int>(
|
|
||||||
suggestions:
|
|
||||||
_filteredSuggestions!.correspondents,
|
|
||||||
itemBuilder: (context, itemData) =>
|
|
||||||
ActionChip(
|
|
||||||
label: Text(
|
|
||||||
state.correspondents[itemData]!.name),
|
|
||||||
onPressed: () {
|
|
||||||
_formKey
|
|
||||||
.currentState?.fields[fkCorrespondent]
|
|
||||||
?.didChange(
|
|
||||||
IdQueryParameter.fromId(itemData),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(),
|
|
||||||
// DocumentType form field
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
LabelFormField<DocumentType>(
|
|
||||||
showAnyAssignedOption: false,
|
|
||||||
showNotAssignedOption: false,
|
|
||||||
addLabelPageBuilder: (currentInput) =>
|
|
||||||
RepositoryProvider.value(
|
|
||||||
value: context.read<LabelRepository>(),
|
|
||||||
child: AddDocumentTypePage(
|
|
||||||
initialName: currentInput,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
canCreateNewLabel: LocalUserAccount.current
|
|
||||||
.paperlessUser.canCreateDocumentTypes,
|
|
||||||
addLabelText: S.of(context)!.addDocumentType,
|
|
||||||
labelText: S.of(context)!.documentType,
|
|
||||||
initialValue:
|
|
||||||
state.document.documentType != null
|
|
||||||
? IdQueryParameter.fromId(
|
|
||||||
state.document.documentType!)
|
|
||||||
: const IdQueryParameter.unset(),
|
|
||||||
options: state.documentTypes,
|
|
||||||
name: _DocumentEditPageState.fkDocumentType,
|
|
||||||
prefixIcon:
|
|
||||||
const Icon(Icons.description_outlined),
|
|
||||||
allowSelectUnassigned: true,
|
|
||||||
),
|
|
||||||
if (_filteredSuggestions
|
|
||||||
?.hasSuggestedDocumentTypes ??
|
|
||||||
false)
|
|
||||||
_buildSuggestionsSkeleton<int>(
|
|
||||||
suggestions:
|
|
||||||
_filteredSuggestions!.documentTypes,
|
|
||||||
itemBuilder: (context, itemData) =>
|
|
||||||
ActionChip(
|
|
||||||
label: Text(
|
|
||||||
state.documentTypes[itemData]!.name),
|
|
||||||
onPressed: () => _formKey
|
|
||||||
.currentState?.fields[fkDocumentType]
|
|
||||||
?.didChange(
|
|
||||||
IdQueryParameter.fromId(itemData),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(),
|
|
||||||
// StoragePath form field
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
LabelFormField<StoragePath>(
|
|
||||||
showAnyAssignedOption: false,
|
|
||||||
showNotAssignedOption: false,
|
|
||||||
addLabelPageBuilder: (initialValue) =>
|
|
||||||
RepositoryProvider.value(
|
|
||||||
value: context.read<LabelRepository>(),
|
|
||||||
child: AddStoragePathPage(
|
|
||||||
initalName: initialValue),
|
|
||||||
),
|
|
||||||
canCreateNewLabel: LocalUserAccount.current
|
|
||||||
.paperlessUser.canCreateStoragePaths,
|
|
||||||
addLabelText: S.of(context)!.addStoragePath,
|
|
||||||
labelText: S.of(context)!.storagePath,
|
|
||||||
options: state.storagePaths,
|
|
||||||
initialValue: state.document.storagePath != null
|
|
||||||
? IdQueryParameter.fromId(
|
|
||||||
state.document.storagePath!)
|
|
||||||
: const IdQueryParameter.unset(),
|
|
||||||
name: fkStoragePath,
|
|
||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
|
||||||
allowSelectUnassigned: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(),
|
|
||||||
// Tag form field
|
|
||||||
TagsFormField(
|
|
||||||
options: state.tags,
|
|
||||||
name: fkTags,
|
|
||||||
allowOnlySelection: true,
|
|
||||||
allowCreation: true,
|
|
||||||
allowExclude: false,
|
|
||||||
initialValue: TagsQuery.ids(
|
|
||||||
include: state.document.tags.toList(),
|
|
||||||
),
|
|
||||||
).padded(),
|
|
||||||
if (_filteredSuggestions?.tags
|
|
||||||
.toSet()
|
|
||||||
.difference(state.document.tags.toSet())
|
|
||||||
.isNotEmpty ??
|
|
||||||
false)
|
|
||||||
_buildSuggestionsSkeleton<int>(
|
|
||||||
suggestions:
|
|
||||||
(_filteredSuggestions?.tags.toSet() ?? {}),
|
|
||||||
itemBuilder: (context, itemData) {
|
|
||||||
final tag = state.tags[itemData]!;
|
|
||||||
return ActionChip(
|
|
||||||
label: Text(
|
|
||||||
tag.name,
|
|
||||||
style: TextStyle(color: tag.textColor),
|
|
||||||
),
|
|
||||||
backgroundColor: tag.color,
|
|
||||||
onPressed: () {
|
|
||||||
final currentTags = _formKey.currentState
|
|
||||||
?.fields[fkTags]?.value as TagsQuery;
|
|
||||||
_formKey.currentState?.fields[fkTags]
|
|
||||||
?.didChange(
|
|
||||||
currentTags.maybeWhen(
|
|
||||||
ids: (include, exclude) =>
|
|
||||||
TagsQuery.ids(
|
|
||||||
include: [...include, itemData],
|
|
||||||
exclude: exclude),
|
|
||||||
orElse: () =>
|
|
||||||
TagsQuery.ids(include: [itemData]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Prevent tags from being hidden by fab
|
|
||||||
const SizedBox(height: 64),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
FormBuilderTextField(
|
|
||||||
name: fkContent,
|
|
||||||
maxLines: null,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
initialValue: state.document.content,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 84),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
extendBody: true,
|
||||||
);
|
body: Padding(
|
||||||
},
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
|
child: FormBuilder(
|
||||||
|
key: _formKey,
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
ListView(
|
||||||
|
children: [
|
||||||
|
_buildTitleFormField(state.document.title).padded(),
|
||||||
|
_buildCreatedAtFormField(
|
||||||
|
state.document.created,
|
||||||
|
filteredSuggestions,
|
||||||
|
).padded(),
|
||||||
|
// Correspondent form field
|
||||||
|
if (currentUser.canViewCorrespondents)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
LabelFormField<Correspondent>(
|
||||||
|
showAnyAssignedOption: false,
|
||||||
|
showNotAssignedOption: false,
|
||||||
|
addLabelPageBuilder: (initialValue) =>
|
||||||
|
RepositoryProvider.value(
|
||||||
|
value: context.read<LabelRepository>(),
|
||||||
|
child: AddCorrespondentPage(
|
||||||
|
initialName: initialValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
addLabelText:
|
||||||
|
S.of(context)!.addCorrespondent,
|
||||||
|
labelText: S.of(context)!.correspondent,
|
||||||
|
options: context
|
||||||
|
.watch<DocumentEditCubit>()
|
||||||
|
.state
|
||||||
|
.correspondents,
|
||||||
|
initialValue:
|
||||||
|
state.document.correspondent != null
|
||||||
|
? IdQueryParameter.fromId(
|
||||||
|
state.document.correspondent!)
|
||||||
|
: const IdQueryParameter.unset(),
|
||||||
|
name: fkCorrespondent,
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.person_outlined),
|
||||||
|
allowSelectUnassigned: true,
|
||||||
|
canCreateNewLabel:
|
||||||
|
currentUser.canCreateCorrespondents,
|
||||||
|
),
|
||||||
|
if (filteredSuggestions
|
||||||
|
?.hasSuggestedCorrespondents ??
|
||||||
|
false)
|
||||||
|
_buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions:
|
||||||
|
filteredSuggestions!.correspondents,
|
||||||
|
itemBuilder: (context, itemData) =>
|
||||||
|
ActionChip(
|
||||||
|
label: Text(state
|
||||||
|
.correspondents[itemData]!.name),
|
||||||
|
onPressed: () {
|
||||||
|
_formKey.currentState
|
||||||
|
?.fields[fkCorrespondent]
|
||||||
|
?.didChange(
|
||||||
|
IdQueryParameter.fromId(itemData),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(),
|
||||||
|
// DocumentType form field
|
||||||
|
if (currentUser.canViewDocumentTypes)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
LabelFormField<DocumentType>(
|
||||||
|
showAnyAssignedOption: false,
|
||||||
|
showNotAssignedOption: false,
|
||||||
|
addLabelPageBuilder: (currentInput) =>
|
||||||
|
RepositoryProvider.value(
|
||||||
|
value: context.read<LabelRepository>(),
|
||||||
|
child: AddDocumentTypePage(
|
||||||
|
initialName: currentInput,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
canCreateNewLabel:
|
||||||
|
currentUser.canCreateDocumentTypes,
|
||||||
|
addLabelText:
|
||||||
|
S.of(context)!.addDocumentType,
|
||||||
|
labelText: S.of(context)!.documentType,
|
||||||
|
initialValue:
|
||||||
|
state.document.documentType != null
|
||||||
|
? IdQueryParameter.fromId(
|
||||||
|
state.document.documentType!)
|
||||||
|
: const IdQueryParameter.unset(),
|
||||||
|
options: state.documentTypes,
|
||||||
|
name: _DocumentEditPageState.fkDocumentType,
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.description_outlined),
|
||||||
|
allowSelectUnassigned: true,
|
||||||
|
),
|
||||||
|
if (filteredSuggestions
|
||||||
|
?.hasSuggestedDocumentTypes ??
|
||||||
|
false)
|
||||||
|
_buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions:
|
||||||
|
filteredSuggestions!.documentTypes,
|
||||||
|
itemBuilder: (context, itemData) =>
|
||||||
|
ActionChip(
|
||||||
|
label: Text(state
|
||||||
|
.documentTypes[itemData]!.name),
|
||||||
|
onPressed: () => _formKey.currentState
|
||||||
|
?.fields[fkDocumentType]
|
||||||
|
?.didChange(
|
||||||
|
IdQueryParameter.fromId(itemData),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(),
|
||||||
|
// StoragePath form field
|
||||||
|
if (currentUser.canViewStoragePaths)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
LabelFormField<StoragePath>(
|
||||||
|
showAnyAssignedOption: false,
|
||||||
|
showNotAssignedOption: false,
|
||||||
|
addLabelPageBuilder: (initialValue) =>
|
||||||
|
RepositoryProvider.value(
|
||||||
|
value: context.read<LabelRepository>(),
|
||||||
|
child: AddStoragePathPage(
|
||||||
|
initialName: initialValue),
|
||||||
|
),
|
||||||
|
canCreateNewLabel:
|
||||||
|
currentUser.canCreateStoragePaths,
|
||||||
|
addLabelText: S.of(context)!.addStoragePath,
|
||||||
|
labelText: S.of(context)!.storagePath,
|
||||||
|
options: state.storagePaths,
|
||||||
|
initialValue:
|
||||||
|
state.document.storagePath != null
|
||||||
|
? IdQueryParameter.fromId(
|
||||||
|
state.document.storagePath!)
|
||||||
|
: const IdQueryParameter.unset(),
|
||||||
|
name: fkStoragePath,
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.folder_outlined),
|
||||||
|
allowSelectUnassigned: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(),
|
||||||
|
// Tag form field
|
||||||
|
if (currentUser.canViewTags)
|
||||||
|
TagsFormField(
|
||||||
|
options: state.tags,
|
||||||
|
name: fkTags,
|
||||||
|
allowOnlySelection: true,
|
||||||
|
allowCreation: true,
|
||||||
|
allowExclude: false,
|
||||||
|
initialValue: TagsQuery.ids(
|
||||||
|
include: state.document.tags.toList(),
|
||||||
|
),
|
||||||
|
).padded(),
|
||||||
|
if (filteredSuggestions?.tags
|
||||||
|
.toSet()
|
||||||
|
.difference(state.document.tags.toSet())
|
||||||
|
.isNotEmpty ??
|
||||||
|
false)
|
||||||
|
_buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions:
|
||||||
|
(filteredSuggestions?.tags.toSet() ?? {}),
|
||||||
|
itemBuilder: (context, itemData) {
|
||||||
|
final tag = state.tags[itemData]!;
|
||||||
|
return ActionChip(
|
||||||
|
label: Text(
|
||||||
|
tag.name,
|
||||||
|
style: TextStyle(color: tag.textColor),
|
||||||
|
),
|
||||||
|
backgroundColor: tag.color,
|
||||||
|
onPressed: () {
|
||||||
|
final currentTags = _formKey.currentState
|
||||||
|
?.fields[fkTags]?.value as TagsQuery;
|
||||||
|
_formKey.currentState?.fields[fkTags]
|
||||||
|
?.didChange(
|
||||||
|
currentTags.maybeWhen(
|
||||||
|
ids: (include, exclude) =>
|
||||||
|
TagsQuery.ids(include: [
|
||||||
|
...include,
|
||||||
|
itemData
|
||||||
|
], exclude: exclude),
|
||||||
|
orElse: () => TagsQuery.ids(
|
||||||
|
include: [itemData]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Prevent tags from being hidden by fab
|
||||||
|
const SizedBox(height: 64),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: fkContent,
|
||||||
|
maxLines: null,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
initialValue: state.document.content,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 84),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,28 +304,23 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
var mergedDocument = document.copyWith(
|
var mergedDocument = document.copyWith(
|
||||||
title: values[fkTitle],
|
title: values[fkTitle],
|
||||||
created: values[fkCreatedDate],
|
created: values[fkCreatedDate],
|
||||||
documentType: () => (values[fkDocumentType] as IdQueryParameter)
|
documentType: () => (values[fkDocumentType] as IdQueryParameter?)
|
||||||
.whenOrNull(fromId: (id) => id),
|
?.whenOrNull(fromId: (id) => id),
|
||||||
correspondent: () => (values[fkCorrespondent] as IdQueryParameter)
|
correspondent: () => (values[fkCorrespondent] as IdQueryParameter?)
|
||||||
.whenOrNull(fromId: (id) => id),
|
?.whenOrNull(fromId: (id) => id),
|
||||||
storagePath: () => (values[fkStoragePath] as IdQueryParameter)
|
storagePath: () => (values[fkStoragePath] as IdQueryParameter?)
|
||||||
.whenOrNull(fromId: (id) => id),
|
?.whenOrNull(fromId: (id) => id),
|
||||||
tags: (values[fkTags] as IdsTagsQuery).include,
|
tags: (values[fkTags] as IdsTagsQuery?)?.include,
|
||||||
content: values[fkContent],
|
content: values[fkContent],
|
||||||
);
|
);
|
||||||
setState(() {
|
|
||||||
_isSubmitLoading = true;
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
|
await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
|
||||||
showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated);
|
showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() {
|
context.pop();
|
||||||
_isSubmitLoading = false;
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +341,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
|
Widget _buildCreatedAtFormField(
|
||||||
|
DateTime? initialCreatedAtDate, FieldSuggestions? filteredSuggestions) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -358,9 +357,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
format: DateFormat.yMMMMd(),
|
format: DateFormat.yMMMMd(),
|
||||||
initialEntryMode: DatePickerEntryMode.calendar,
|
initialEntryMode: DatePickerEntryMode.calendar,
|
||||||
),
|
),
|
||||||
if (_filteredSuggestions?.hasSuggestedDates ?? false)
|
if (filteredSuggestions?.hasSuggestedDates ?? false)
|
||||||
_buildSuggestionsSkeleton<DateTime>(
|
_buildSuggestionsSkeleton<DateTime>(
|
||||||
suggestions: _filteredSuggestions!.dates,
|
suggestions: filteredSuggestions!.dates,
|
||||||
itemBuilder: (context, itemData) => ActionChip(
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
label: Text(DateFormat.yMMMd().format(itemData)),
|
label: Text(DateFormat.yMMMd().format(itemData)),
|
||||||
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
||||||
|
|||||||
@@ -1,43 +1,71 @@
|
|||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
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';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.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:rxdart/rxdart.dart';
|
||||||
|
|
||||||
class DocumentScannerCubit extends Cubit<List<File>> {
|
part 'document_scanner_state.dart';
|
||||||
|
|
||||||
|
class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
||||||
final LocalNotificationService _notificationService;
|
final LocalNotificationService _notificationService;
|
||||||
|
|
||||||
DocumentScannerCubit(this._notificationService) : super(const []);
|
DocumentScannerCubit(this._notificationService)
|
||||||
|
: super(const InitialDocumentScannerState());
|
||||||
|
|
||||||
void addScan(File file) => emit([...state, file]);
|
Future<void> initialize() async {
|
||||||
|
debugPrint("Restoring scans...");
|
||||||
void removeScan(int fileIndex) {
|
emit(const RestoringDocumentScannerState());
|
||||||
try {
|
final tempDir = await FileService.temporaryScansDirectory;
|
||||||
state[fileIndex].deleteSync();
|
final allFiles = tempDir.list().whereType<File>();
|
||||||
final scans = [...state];
|
final scans =
|
||||||
scans.removeAt(fileIndex);
|
await allFiles.where((event) => event.path.endsWith(".jpeg")).toList();
|
||||||
emit(scans);
|
debugPrint("Restored ${scans.length} scans.");
|
||||||
} catch (_) {
|
emit(
|
||||||
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
scans.isEmpty
|
||||||
}
|
? const InitialDocumentScannerState()
|
||||||
|
: LoadedDocumentScannerState(scans: scans),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void addScan(File file) async {
|
||||||
|
emit(LoadedDocumentScannerState(
|
||||||
|
scans: [...state.scans, file],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeScan(File file) async {
|
||||||
try {
|
try {
|
||||||
for (final doc in state) {
|
await file.delete();
|
||||||
doc.deleteSync();
|
} catch (error, stackTrace) {
|
||||||
if (kDebugMode) {
|
throw InfoMessageException(
|
||||||
log('[ScannerCubit]: Removed ${doc.path}');
|
code: ErrorCode.scanRemoveFailed,
|
||||||
}
|
message: error.toString(),
|
||||||
}
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final scans = state.scans..remove(file);
|
||||||
|
emit(
|
||||||
|
scans.isEmpty
|
||||||
|
? const InitialDocumentScannerState()
|
||||||
|
: LoadedDocumentScannerState(scans: scans),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reset() async {
|
||||||
|
try {
|
||||||
|
Future.wait([
|
||||||
|
for (final file in state.scans) file.delete(),
|
||||||
|
]);
|
||||||
imageCache.clear();
|
imageCache.clear();
|
||||||
emit([]);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
||||||
|
} finally {
|
||||||
|
emit(const InitialDocumentScannerState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
part of 'document_scanner_cubit.dart';
|
||||||
|
|
||||||
|
sealed class DocumentScannerState {
|
||||||
|
final List<File> scans;
|
||||||
|
|
||||||
|
const DocumentScannerState({
|
||||||
|
this.scans = const [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class InitialDocumentScannerState extends DocumentScannerState {
|
||||||
|
const InitialDocumentScannerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestoringDocumentScannerState extends DocumentScannerState {
|
||||||
|
const RestoringDocumentScannerState({super.scans});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadedDocumentScannerState extends DocumentScannerState {
|
||||||
|
const LoadedDocumentScannerState({super.scans});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorDocumentScannerState extends DocumentScannerState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const ErrorDocumentScannerState({
|
||||||
|
required this.message,
|
||||||
|
super.scans,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:image/image.dart' as im;
|
|
||||||
|
|
||||||
typedef ImageOperationCallback = im.Image Function(im.Image);
|
|
||||||
|
|
||||||
class DecodeParam {
|
|
||||||
final File file;
|
|
||||||
final SendPort sendPort;
|
|
||||||
final im.Image Function(im.Image) imageOperation;
|
|
||||||
DecodeParam(this.file, this.sendPort, this.imageOperation);
|
|
||||||
}
|
|
||||||
|
|
||||||
void decodeIsolate(DecodeParam param) {
|
|
||||||
// Read an image from file (webp in this case).
|
|
||||||
// decodeImage will identify the format of the image and use the appropriate
|
|
||||||
// decoder.
|
|
||||||
var image = im.decodeImage(param.file.readAsBytesSync())!;
|
|
||||||
// Resize the image to a 120x? thumbnail (maintaining the aspect ratio).
|
|
||||||
var processed = param.imageOperation(image);
|
|
||||||
param.sendPort.send(processed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode and process an image file in a separate thread (isolate) to avoid
|
|
||||||
// stalling the main UI thread.
|
|
||||||
Future<File> processImage(
|
|
||||||
File file,
|
|
||||||
ImageOperationCallback imageOperation,
|
|
||||||
) async {
|
|
||||||
var receivePort = ReceivePort();
|
|
||||||
|
|
||||||
await Isolate.spawn(
|
|
||||||
decodeIsolate,
|
|
||||||
DecodeParam(
|
|
||||||
file,
|
|
||||||
receivePort.sendPort,
|
|
||||||
imageOperation,
|
|
||||||
));
|
|
||||||
|
|
||||||
var image = await receivePort.first as im.Image;
|
|
||||||
|
|
||||||
return file.writeAsBytes(im.encodePng(image));
|
|
||||||
}
|
|
||||||
@@ -10,23 +10,22 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/global/constants.dart';
|
import 'package:paperless_mobile/core/global/constants.dart';
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_description.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
|
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_scan/view/widgets/export_scans_dialog.dart';
|
import 'package:paperless_mobile/features/document_scan/view/widgets/export_scans_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart';
|
import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:pdf/pdf.dart';
|
import 'package:pdf/pdf.dart';
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
@@ -51,71 +50,54 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return SafeArea(
|
||||||
builder: (context, connectedState) {
|
top: true,
|
||||||
return Scaffold(
|
child: Scaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => _openDocumentScanner(context),
|
heroTag: "fab_document_edit",
|
||||||
child: const Icon(Icons.add_a_photo_outlined),
|
onPressed: () => _openDocumentScanner(context),
|
||||||
),
|
child: const Icon(Icons.add_a_photo_outlined),
|
||||||
body: BlocBuilder<DocumentScannerCubit, List<File>>(
|
),
|
||||||
|
body: NestedScrollView(
|
||||||
|
floatHeaderSlivers: true,
|
||||||
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
SliverOverlapAbsorber(
|
||||||
|
handle: searchBarHandle,
|
||||||
|
sliver: SliverSearchBar(
|
||||||
|
titleText: S.of(context)!.scanner,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverOverlapAbsorber(
|
||||||
|
handle: actionsHandle,
|
||||||
|
sliver: SliverPinnedHeader(
|
||||||
|
child: _buildActions(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: BlocBuilder<DocumentScannerCubit, DocumentScannerState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SafeArea(
|
return switch (state) {
|
||||||
child: Scaffold(
|
InitialDocumentScannerState() => _buildEmptyState(),
|
||||||
drawer: const AppDrawer(),
|
RestoringDocumentScannerState() => Center(
|
||||||
floatingActionButton: FloatingActionButton(
|
child: Text("Restoring..."),
|
||||||
onPressed: () => _openDocumentScanner(context),
|
|
||||||
child: const Icon(Icons.add_a_photo_outlined),
|
|
||||||
),
|
),
|
||||||
body: NestedScrollView(
|
LoadedDocumentScannerState() => _buildImageGrid(state.scans),
|
||||||
floatHeaderSlivers: true,
|
ErrorDocumentScannerState() => Placeholder(),
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
};
|
||||||
SliverOverlapAbsorber(
|
|
||||||
handle: searchBarHandle,
|
|
||||||
sliver: SliverSearchBar(
|
|
||||||
titleText: S.of(context)!.scanner,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverOverlapAbsorber(
|
|
||||||
handle: actionsHandle,
|
|
||||||
sliver: SliverPinnedHeader(
|
|
||||||
child: _buildActions(connectedState.isConnected),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
body: BlocBuilder<DocumentScannerCubit, List<File>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.isEmpty) {
|
|
||||||
return SizedBox.expand(
|
|
||||||
child: Center(
|
|
||||||
child: _buildEmptyState(
|
|
||||||
connectedState.isConnected,
|
|
||||||
state,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return _buildImageGrid(state);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActions(bool isConnected) {
|
Widget _buildActions() {
|
||||||
return ColoredBox(
|
return ColoredBox(
|
||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: kTextTabBarHeight,
|
height: kTextTabBarHeight,
|
||||||
child: BlocBuilder<DocumentScannerCubit, List<File>>(
|
child: BlocBuilder<DocumentScannerCubit, DocumentScannerState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return RawScrollbar(
|
return RawScrollbar(
|
||||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 4),
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 4),
|
||||||
@@ -134,12 +116,12 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
),
|
),
|
||||||
onPressed: state.isNotEmpty
|
onPressed: state.scans.isNotEmpty
|
||||||
? () => Navigator.of(context).push(
|
? () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => DocumentView(
|
builder: (context) => DocumentView(
|
||||||
documentBytes: _assembleFileBytes(
|
documentBytes: _assembleFileBytes(
|
||||||
state,
|
state.scans,
|
||||||
forcePdf: true,
|
forcePdf: true,
|
||||||
).then((file) => file.bytes),
|
).then((file) => file.bytes),
|
||||||
),
|
),
|
||||||
@@ -154,19 +136,32 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
),
|
),
|
||||||
onPressed: state.isEmpty ? null : () => _reset(context),
|
onPressed:
|
||||||
|
state.scans.isEmpty ? null : () => _reset(context),
|
||||||
icon: const Icon(Icons.delete_sweep_outlined),
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
TextButton.icon(
|
ConnectivityAwareActionWrapper(
|
||||||
label: Text(S.of(context)!.upload),
|
offlineBuilder: (context, child) {
|
||||||
style: TextButton.styleFrom(
|
return TextButton.icon(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
label: Text(S.of(context)!.upload),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
|
),
|
||||||
|
onPressed: null,
|
||||||
|
icon: const Icon(Icons.upload_outlined),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
disabled: state.scans.isEmpty,
|
||||||
|
child: TextButton.icon(
|
||||||
|
label: Text(S.of(context)!.upload),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
_onPrepareDocumentUpload(context, state.scans),
|
||||||
|
icon: const Icon(Icons.upload_outlined),
|
||||||
),
|
),
|
||||||
onPressed: state.isEmpty || !isConnected
|
|
||||||
? null
|
|
||||||
: () => _onPrepareDocumentUpload(context),
|
|
||||||
icon: const Icon(Icons.upload_outlined),
|
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
@@ -174,7 +169,7 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
),
|
),
|
||||||
onPressed: state.isEmpty ? null : _onSaveToFile,
|
onPressed: state.scans.isEmpty ? null : _onSaveToFile,
|
||||||
icon: const Icon(Icons.save_alt_outlined),
|
icon: const Icon(Icons.save_alt_outlined),
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
@@ -196,7 +191,7 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
final cubit = context.read<DocumentScannerCubit>();
|
final cubit = context.read<DocumentScannerCubit>();
|
||||||
final file = await _assembleFileBytes(
|
final file = await _assembleFileBytes(
|
||||||
forcePdf: true,
|
forcePdf: true,
|
||||||
context.read<DocumentScannerCubit>().state,
|
context.read<DocumentScannerCubit>().state.scans,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
final globalSettings =
|
final globalSettings =
|
||||||
@@ -253,31 +248,27 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
context.read<DocumentScannerCubit>().addScan(file);
|
context.read<DocumentScannerCubit>().addScan(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPrepareDocumentUpload(BuildContext context) async {
|
void _onPrepareDocumentUpload(BuildContext context, List<File> scans) async {
|
||||||
final file = await _assembleFileBytes(
|
final file = await _assembleFileBytes(
|
||||||
context.read<DocumentScannerCubit>().state,
|
scans,
|
||||||
forcePdf: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
forcePdf: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||||
.getValue()!
|
.getValue()!
|
||||||
.enforceSinglePagePdfUpload,
|
.enforceSinglePagePdfUpload,
|
||||||
);
|
);
|
||||||
final uploadResult = await pushDocumentUploadPreparationPage(
|
final uploadResult = await DocumentUploadRoute(
|
||||||
context,
|
$extra: file.bytes,
|
||||||
bytes: file.bytes,
|
|
||||||
fileExtension: file.extension,
|
fileExtension: file.extension,
|
||||||
);
|
).push<DocumentUploadResult>(context);
|
||||||
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
|
if (uploadResult?.success ?? false) {
|
||||||
// For paperless version older than 1.11.3, task id will always be null!
|
// For paperless version older than 1.11.3, task id will always be null!
|
||||||
context.read<DocumentScannerCubit>().reset();
|
context.read<DocumentScannerCubit>().reset();
|
||||||
context
|
// context
|
||||||
.read<TaskStatusCubit>()
|
// .read<PendingTasksNotifier>()
|
||||||
.listenToTaskChanges(uploadResult!.taskId!);
|
// .listenToTaskChanges(uploadResult!.taskId!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(bool isConnected, List<File> scans) {
|
Widget _buildEmptyState() {
|
||||||
if (scans.isNotEmpty) {
|
|
||||||
return _buildImageGrid(scans);
|
|
||||||
}
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@@ -293,9 +284,15 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
onPressed: () => _openDocumentScanner(context),
|
onPressed: () => _openDocumentScanner(context),
|
||||||
),
|
),
|
||||||
Text(S.of(context)!.or),
|
Text(S.of(context)!.or),
|
||||||
TextButton(
|
ConnectivityAwareActionWrapper(
|
||||||
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
|
offlineBuilder: (context, child) => TextButton(
|
||||||
onPressed: isConnected ? _onUploadFromFilesystem : null,
|
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
|
||||||
|
onPressed: null,
|
||||||
|
),
|
||||||
|
child: TextButton(
|
||||||
|
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
|
||||||
|
onPressed: _onUploadFromFilesystem,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -323,7 +320,9 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
file: scans[index],
|
file: scans[index],
|
||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
try {
|
try {
|
||||||
context.read<DocumentScannerCubit>().removeScan(index);
|
context
|
||||||
|
.read<DocumentScannerCubit>()
|
||||||
|
.removeScan(scans[index]);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
@@ -349,30 +348,34 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
void _onUploadFromFilesystem() async {
|
void _onUploadFromFilesystem() async {
|
||||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: supportedFileExtensions,
|
allowedExtensions:
|
||||||
|
supportedFileExtensions.map((e) => e.replaceAll(".", "")).toList(),
|
||||||
withData: true,
|
withData: true,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
);
|
);
|
||||||
if (result?.files.single.path != null) {
|
if (result?.files.single.path != null) {
|
||||||
final path = result!.files.single.path!;
|
final path = result!.files.single.path!;
|
||||||
final fileDescription = FileDescription.fromPath(path);
|
final extension = p.extension(path);
|
||||||
|
final filename = p.basenameWithoutExtension(path);
|
||||||
File file = File(path);
|
File file = File(path);
|
||||||
if (!supportedFileExtensions.contains(
|
if (!supportedFileExtensions.contains(extension.toLowerCase())) {
|
||||||
fileDescription.extension.toLowerCase(),
|
|
||||||
)) {
|
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
context,
|
context,
|
||||||
const PaperlessApiException(ErrorCode.unsupportedFileFormat),
|
const PaperlessApiException(ErrorCode.unsupportedFileFormat),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pushDocumentUploadPreparationPage(
|
DocumentUploadRoute(
|
||||||
context,
|
$extra: file.readAsBytesSync(),
|
||||||
bytes: file.readAsBytesSync(),
|
filename: filename,
|
||||||
filename: fileDescription.filename,
|
title: filename,
|
||||||
title: fileDescription.filename,
|
fileExtension: extension,
|
||||||
fileExtension: fileDescription.extension,
|
).push<DocumentUploadResult>(context);
|
||||||
);
|
// if (uploadResult.success && uploadResult.taskId != null) {
|
||||||
|
// context
|
||||||
|
// .read<PendingTasksNotifier>()
|
||||||
|
// .listenToTaskChanges(uploadResult.taskId!);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/database/tables/local_user_app_state.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
@@ -15,6 +16,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
|
|||||||
with DocumentPagingBlocMixin {
|
with DocumentPagingBlocMixin {
|
||||||
@override
|
@override
|
||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
@@ -24,8 +27,11 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
|
|||||||
this.api,
|
this.api,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
this._userAppState,
|
this._userAppState,
|
||||||
) : super(DocumentSearchState(
|
this.connectivityStatusService,
|
||||||
searchHistory: _userAppState.documentSearchHistory)) {
|
) : super(
|
||||||
|
DocumentSearchState(
|
||||||
|
searchHistory: _userAppState.documentSearchHistory),
|
||||||
|
) {
|
||||||
notifier.addListener(
|
notifier.addListener(
|
||||||
this,
|
this,
|
||||||
onDeleted: remove,
|
onDeleted: remove,
|
||||||
@@ -34,22 +40,25 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> search(String query) async {
|
Future<void> search(String query) async {
|
||||||
emit(state.copyWith(
|
final normalizedQuery = query.trim();
|
||||||
isLoading: true,
|
emit(
|
||||||
suggestions: [],
|
state.copyWith(
|
||||||
view: SearchView.results,
|
isLoading: true,
|
||||||
));
|
suggestions: [],
|
||||||
|
view: SearchView.results,
|
||||||
|
),
|
||||||
|
);
|
||||||
final searchFilter = DocumentFilter(
|
final searchFilter = DocumentFilter(
|
||||||
query: TextQuery.extended(query),
|
query: TextQuery.extended(normalizedQuery),
|
||||||
);
|
);
|
||||||
|
|
||||||
await updateFilter(filter: searchFilter);
|
await updateFilter(filter: searchFilter);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
searchHistory: [
|
searchHistory: [
|
||||||
query,
|
normalizedQuery,
|
||||||
...state.searchHistory
|
...state.searchHistory
|
||||||
.whereNot((previousQuery) => previousQuery == query)
|
.whereNot((previousQuery) => previousQuery == normalizedQuery)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/user_repository.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
||||||
|
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -27,112 +23,96 @@ class DocumentSearchBar extends StatefulWidget {
|
|||||||
class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return OpenContainer(
|
||||||
margin: EdgeInsets.only(top: 8),
|
transitionDuration: const Duration(milliseconds: 200),
|
||||||
child: OpenContainer(
|
transitionType: ContainerTransitionType.fadeThrough,
|
||||||
transitionDuration: const Duration(milliseconds: 200),
|
closedElevation: 1,
|
||||||
transitionType: ContainerTransitionType.fadeThrough,
|
middleColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
closedElevation: 1,
|
openColor: Theme.of(context).colorScheme.background,
|
||||||
middleColor: Theme.of(context).colorScheme.surfaceVariant,
|
closedColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
openColor: Theme.of(context).colorScheme.background,
|
closedShape: RoundedRectangleBorder(
|
||||||
closedColor: Theme.of(context).colorScheme.surfaceVariant,
|
borderRadius: BorderRadius.circular(56),
|
||||||
closedShape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(56),
|
closedBuilder: (_, action) {
|
||||||
),
|
return InkWell(
|
||||||
closedBuilder: (_, action) {
|
onTap: action,
|
||||||
return InkWell(
|
child: ConstrainedBox(
|
||||||
onTap: action,
|
constraints: const BoxConstraints(
|
||||||
child: ConstrainedBox(
|
maxWidth: 720,
|
||||||
constraints: const BoxConstraints(
|
minWidth: 360,
|
||||||
maxWidth: 720,
|
maxHeight: 56,
|
||||||
minWidth: 360,
|
minHeight: 48,
|
||||||
maxHeight: 56,
|
),
|
||||||
minHeight: 48,
|
child: Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
child: Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Flexible(
|
||||||
children: [
|
child: Padding(
|
||||||
Flexible(
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Row(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
IconButton(
|
||||||
children: [
|
icon: ListenableBuilder(
|
||||||
IconButton(
|
listenable:
|
||||||
icon: const Icon(Icons.menu),
|
context.read<ConsumptionChangeNotifier>(),
|
||||||
onPressed: Scaffold.of(context).openDrawer,
|
builder: (context, child) {
|
||||||
|
return Badge(
|
||||||
|
isLabelVisible: context
|
||||||
|
.read<ConsumptionChangeNotifier>()
|
||||||
|
.pendingFiles
|
||||||
|
.isNotEmpty,
|
||||||
|
child: const Icon(Icons.menu),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
smallSize: 8,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Flexible(
|
onPressed: Scaffold.of(context).openDrawer,
|
||||||
child: Text(
|
),
|
||||||
S.of(context)!.searchDocuments,
|
Flexible(
|
||||||
style: Theme.of(context)
|
child: Text(
|
||||||
.textTheme
|
S.of(context)!.searchDocuments,
|
||||||
.bodyLarge
|
style:
|
||||||
?.copyWith(
|
Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildUserAvatar(context),
|
),
|
||||||
],
|
_buildUserAvatar(context),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
openBuilder: (_, action) {
|
},
|
||||||
return MultiProvider(
|
openBuilder: (_, action) {
|
||||||
providers: [
|
return Provider(
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
create: (_) => DocumentSearchCubit(
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
context.read(),
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
context.read(),
|
||||||
Provider.value(value: context.read<ApiVersion>()),
|
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
||||||
if (context.read<ApiVersion>().hasMultiUserSupport)
|
.get(context.read<LocalUserAccount>().id)!,
|
||||||
Provider.value(value: context.read<UserRepository>()),
|
context.read(),
|
||||||
],
|
),
|
||||||
child: Provider(
|
child: const DocumentSearchPage(),
|
||||||
create: (_) => DocumentSearchCubit(
|
);
|
||||||
context.read(),
|
},
|
||||||
context.read(),
|
|
||||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
|
||||||
.get(LocalUserAccount.current.id)!,
|
|
||||||
),
|
|
||||||
builder: (_, __) => const DocumentSearchPage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton _buildUserAvatar(BuildContext context) {
|
IconButton _buildUserAvatar(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
icon: GlobalSettingsBuilder(
|
icon: UserAvatar(account: context.watch<LocalUserAccount>()),
|
||||||
builder: (context, settings) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable:
|
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
|
||||||
.listenable(),
|
|
||||||
builder: (context, box, _) {
|
|
||||||
final account = box.get(settings.currentLoggedInUser!)!;
|
|
||||||
return UserAvatar(account: account);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final apiVersion = context.read<ApiVersion>();
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Provider.value(
|
builder: (_) => const ManageAccountsPage(),
|
||||||
value: apiVersion,
|
|
||||||
child: const ManageAccountsPage(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import 'dart:math' as math;
|
|||||||
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_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.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_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
|
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
|
||||||
class DocumentSearchPage extends StatefulWidget {
|
class DocumentSearchPage extends StatefulWidget {
|
||||||
const DocumentSearchPage({super.key});
|
const DocumentSearchPage({super.key});
|
||||||
@@ -186,7 +186,7 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
S.of(context)!.results,
|
S.of(context)!.results,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
),
|
),
|
||||||
BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
|
BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@@ -198,15 +198,15 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).padded();
|
).paddedLTRB(16, 8, 8, 8);
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(child: header),
|
SliverToBoxAdapter(child: header),
|
||||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty)
|
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(S.of(context)!.noMatchesFound),
|
child: Text(S.of(context)!.noDocumentsFound),
|
||||||
),
|
).paddedOnly(top: 8),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
SliverAdaptiveDocumentsView(
|
SliverAdaptiveDocumentsView(
|
||||||
@@ -218,11 +218,8 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
hasLoaded: state.hasLoaded,
|
hasLoaded: state.hasLoaded,
|
||||||
enableHeroAnimation: false,
|
enableHeroAnimation: false,
|
||||||
onTap: (document) {
|
onTap: (document) {
|
||||||
pushDocumentDetailsRoute(
|
DocumentDetailsRoute($extra: document, isLabelClickable: false)
|
||||||
context,
|
.push(context);
|
||||||
document: document,
|
|
||||||
isLabelClickable: false,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart';
|
import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
||||||
@@ -25,14 +22,11 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (LocalUserAccount.current.paperlessUser.canViewDocuments) {
|
if (context.watch<LocalUserAccount>().paperlessUser.canViewDocuments) {
|
||||||
return SliverAppBar(
|
return const SliverAppBar(
|
||||||
toolbarHeight: kToolbarHeight,
|
titleSpacing: 8,
|
||||||
flexibleSpace: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: const DocumentSearchBar(),
|
|
||||||
),
|
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
title: DocumentSearchBar(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
@@ -49,18 +43,17 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||||
.listenable(),
|
.listenable(),
|
||||||
builder: (context, box, _) {
|
builder: (context, box, _) {
|
||||||
final account = box.get(settings.currentLoggedInUser!)!;
|
final account = box.get(settings.loggedInUserId!)!;
|
||||||
return UserAvatar(account: account);
|
return UserAvatar(account: account);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final apiVersion = context.read<ApiVersion>();
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Provider.value(
|
builder: (_) => Provider.value(
|
||||||
value: apiVersion,
|
value: context.read<LocalUserAccount>(),
|
||||||
child: const ManageAccountsPage(),
|
child: const ManageAccountsPage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.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/service/connectivity_status_service.dart';
|
||||||
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.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 PendingTasksNotifier _tasksNotifier;
|
||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
final Connectivity _connectivity;
|
final ConnectivityStatusService _connectivityStatusService;
|
||||||
|
|
||||||
DocumentUploadCubit(
|
DocumentUploadCubit(
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this._documentApi,
|
this._documentApi,
|
||||||
this._connectivity,
|
this._connectivityStatusService,
|
||||||
|
this._tasksNotifier,
|
||||||
) : super(const DocumentUploadState()) {
|
) : super(const DocumentUploadState()) {
|
||||||
_labelRepository.addListener(
|
_labelRepository.addListener(
|
||||||
this,
|
this,
|
||||||
@@ -43,7 +45,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
int? asn,
|
int? asn,
|
||||||
}) async {
|
}) async {
|
||||||
return await _documentApi.create(
|
final taskId = await _documentApi.create(
|
||||||
bytes,
|
bytes,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -53,6 +55,10 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
asn: asn,
|
asn: asn,
|
||||||
);
|
);
|
||||||
|
if (taskId != null) {
|
||||||
|
_tasksNotifier.listenToTaskChanges(taskId);
|
||||||
|
}
|
||||||
|
return taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
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:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
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';
|
||||||
@@ -12,6 +13,7 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
|||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/future_or_builder.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,8 +21,8 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type
|
|||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||||
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/features/sharing/view/widgets/file_thumbnail.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ class DocumentUploadResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DocumentUploadPreparationPage extends StatefulWidget {
|
class DocumentUploadPreparationPage extends StatefulWidget {
|
||||||
final Uint8List fileBytes;
|
final FutureOr<Uint8List> fileBytes;
|
||||||
final String? title;
|
final String? title;
|
||||||
final String? filename;
|
final String? filename;
|
||||||
final String? fileExtension;
|
final String? fileExtension;
|
||||||
@@ -56,7 +58,6 @@ class _DocumentUploadPreparationPageState
|
|||||||
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
|
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
|
||||||
|
|
||||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||||
|
|
||||||
Map<String, String> _errors = {};
|
Map<String, String> _errors = {};
|
||||||
bool _isUploadLoading = false;
|
bool _isUploadLoading = false;
|
||||||
late bool _syncTitleAndFilename;
|
late bool _syncTitleAndFilename;
|
||||||
@@ -73,18 +74,12 @@ class _DocumentUploadPreparationPageState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
extendBodyBehindAppBar: false,
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(S.of(context)!.prepareDocument),
|
|
||||||
bottom: _isUploadLoading
|
|
||||||
? const PreferredSize(
|
|
||||||
child: LinearProgressIndicator(),
|
|
||||||
preferredSize: Size.fromHeight(4.0))
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
|
heroTag: "fab_document_upload",
|
||||||
onPressed: _onSubmit,
|
onPressed: _onSubmit,
|
||||||
label: Text(S.of(context)!.upload),
|
label: Text(S.of(context)!.upload),
|
||||||
icon: const Icon(Icons.upload),
|
icon: const Icon(Icons.upload),
|
||||||
@@ -94,174 +89,249 @@ class _DocumentUploadPreparationPageState
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return FormBuilder(
|
return FormBuilder(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(
|
child: NestedScrollView(
|
||||||
children: [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
// Title
|
SliverOverlapAbsorber(
|
||||||
FormBuilderTextField(
|
handle:
|
||||||
autovalidateMode: AutovalidateMode.always,
|
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
name: DocumentModel.titleKey,
|
sliver: SliverAppBar(
|
||||||
initialValue:
|
leading: BackButton(),
|
||||||
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
|
pinned: true,
|
||||||
validator: (value) {
|
expandedHeight: 150,
|
||||||
if (value?.trim().isEmpty ?? true) {
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
return S.of(context)!.thisFieldIsRequired;
|
background: FutureOrBuilder<Uint8List>(
|
||||||
}
|
future: widget.fileBytes,
|
||||||
return null;
|
builder: (context, snapshot) {
|
||||||
},
|
if (!snapshot.hasData) {
|
||||||
decoration: InputDecoration(
|
return SizedBox.shrink();
|
||||||
labelText: S.of(context)!.title,
|
}
|
||||||
suffixIcon: IconButton(
|
return FileThumbnail(
|
||||||
icon: const Icon(Icons.close),
|
bytes: snapshot.data!,
|
||||||
onPressed: () {
|
fit: BoxFit.fitWidth,
|
||||||
_formKey.currentState?.fields[DocumentModel.titleKey]
|
width: MediaQuery.sizeOf(context).width,
|
||||||
?.didChange("");
|
);
|
||||||
if (_syncTitleAndFilename) {
|
},
|
||||||
_formKey.currentState?.fields[fkFileName]
|
),
|
||||||
?.didChange("");
|
title: Text(S.of(context)!.prepareDocument),
|
||||||
}
|
collapseMode: CollapseMode.pin,
|
||||||
},
|
|
||||||
),
|
),
|
||||||
errorText: _errors[DocumentModel.titleKey],
|
bottom: _isUploadLoading
|
||||||
),
|
? PreferredSize(
|
||||||
onChanged: (value) {
|
child: LinearProgressIndicator(),
|
||||||
final String transformedValue =
|
preferredSize: Size.fromHeight(4.0),
|
||||||
_formatFilename(value ?? '');
|
|
||||||
if (_syncTitleAndFilename) {
|
|
||||||
_formKey.currentState?.fields[fkFileName]
|
|
||||||
?.didChange(transformedValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Filename
|
|
||||||
FormBuilderTextField(
|
|
||||||
autovalidateMode: AutovalidateMode.always,
|
|
||||||
readOnly: _syncTitleAndFilename,
|
|
||||||
enabled: !_syncTitleAndFilename,
|
|
||||||
name: fkFileName,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: S.of(context)!.fileName,
|
|
||||||
suffixText: widget.fileExtension,
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
onPressed: () => _formKey.currentState?.fields[fkFileName]
|
|
||||||
?.didChange(''),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
initialValue: widget.filename ??
|
|
||||||
"scan_${fileNameDateFormat.format(_now)}",
|
|
||||||
),
|
|
||||||
// Synchronize title and filename
|
|
||||||
SwitchListTile(
|
|
||||||
value: _syncTitleAndFilename,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(
|
|
||||||
() => _syncTitleAndFilename = value,
|
|
||||||
);
|
|
||||||
if (_syncTitleAndFilename) {
|
|
||||||
final String transformedValue = _formatFilename(_formKey
|
|
||||||
.currentState
|
|
||||||
?.fields[DocumentModel.titleKey]
|
|
||||||
?.value as String);
|
|
||||||
if (_syncTitleAndFilename) {
|
|
||||||
_formKey.currentState?.fields[fkFileName]
|
|
||||||
?.didChange(transformedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
S.of(context)!.synchronizeTitleAndFilename,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Created at
|
|
||||||
FormBuilderDateTimePicker(
|
|
||||||
autovalidateMode: AutovalidateMode.always,
|
|
||||||
format: DateFormat.yMMMMd(),
|
|
||||||
inputType: InputType.date,
|
|
||||||
name: DocumentModel.createdKey,
|
|
||||||
initialValue: null,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() => _showDatePickerDeleteIcon = value != null);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
|
||||||
labelText: S.of(context)!.createdAt + " *",
|
|
||||||
suffixIcon: _showDatePickerDeleteIcon
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () {
|
|
||||||
_formKey.currentState!
|
|
||||||
.fields[DocumentModel.createdKey]
|
|
||||||
?.didChange(null);
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Correspondent
|
],
|
||||||
if (LocalUserAccount
|
body: Padding(
|
||||||
.current.paperlessUser.canViewCorrespondents)
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
LabelFormField<Correspondent>(
|
child: Builder(
|
||||||
showAnyAssignedOption: false,
|
builder: (context) {
|
||||||
showNotAssignedOption: false,
|
return CustomScrollView(
|
||||||
addLabelPageBuilder: (initialName) => MultiProvider(
|
slivers: [
|
||||||
providers: [
|
SliverOverlapInjector(
|
||||||
Provider.value(
|
handle:
|
||||||
value: context.read<LabelRepository>(),
|
NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||||
|
context),
|
||||||
),
|
),
|
||||||
Provider.value(
|
SliverList.list(
|
||||||
value: context.read<ApiVersion>(),
|
children: [
|
||||||
)
|
// Title
|
||||||
],
|
FormBuilderTextField(
|
||||||
child: AddCorrespondentPage(initialName: initialName),
|
autovalidateMode: AutovalidateMode.always,
|
||||||
),
|
name: DocumentModel.titleKey,
|
||||||
addLabelText: S.of(context)!.addCorrespondent,
|
initialValue: widget.title ??
|
||||||
labelText: S.of(context)!.correspondent + " *",
|
"scan_${fileNameDateFormat.format(_now)}",
|
||||||
name: DocumentModel.correspondentKey,
|
validator: (value) {
|
||||||
options: state.correspondents,
|
if (value?.trim().isEmpty ?? true) {
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
return S.of(context)!.thisFieldIsRequired;
|
||||||
allowSelectUnassigned: true,
|
}
|
||||||
canCreateNewLabel: LocalUserAccount
|
return null;
|
||||||
.current.paperlessUser.canCreateCorrespondents,
|
},
|
||||||
),
|
decoration: InputDecoration(
|
||||||
// Document type
|
labelText: S.of(context)!.title,
|
||||||
if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
|
suffixIcon: IconButton(
|
||||||
LabelFormField<DocumentType>(
|
icon: const Icon(Icons.close),
|
||||||
showAnyAssignedOption: false,
|
onPressed: () {
|
||||||
showNotAssignedOption: false,
|
_formKey.currentState
|
||||||
addLabelPageBuilder: (initialName) => MultiProvider(
|
?.fields[DocumentModel.titleKey]
|
||||||
providers: [
|
?.didChange("");
|
||||||
Provider.value(
|
if (_syncTitleAndFilename) {
|
||||||
value: context.read<LabelRepository>(),
|
_formKey.currentState?.fields[fkFileName]
|
||||||
|
?.didChange("");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
errorText: _errors[DocumentModel.titleKey],
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
final String transformedValue =
|
||||||
|
_formatFilename(value ?? '');
|
||||||
|
if (_syncTitleAndFilename) {
|
||||||
|
_formKey.currentState?.fields[fkFileName]
|
||||||
|
?.didChange(transformedValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Filename
|
||||||
|
FormBuilderTextField(
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
readOnly: _syncTitleAndFilename,
|
||||||
|
enabled: !_syncTitleAndFilename,
|
||||||
|
name: fkFileName,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: S.of(context)!.fileName,
|
||||||
|
suffixText: widget.fileExtension,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () => _formKey
|
||||||
|
.currentState?.fields[fkFileName]
|
||||||
|
?.didChange(''),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
initialValue: widget.filename ??
|
||||||
|
"scan_${fileNameDateFormat.format(_now)}",
|
||||||
|
),
|
||||||
|
// Synchronize title and filename
|
||||||
|
SwitchListTile(
|
||||||
|
value: _syncTitleAndFilename,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(
|
||||||
|
() => _syncTitleAndFilename = value,
|
||||||
|
);
|
||||||
|
if (_syncTitleAndFilename) {
|
||||||
|
final String transformedValue =
|
||||||
|
_formatFilename(_formKey
|
||||||
|
.currentState
|
||||||
|
?.fields[DocumentModel.titleKey]
|
||||||
|
?.value as String);
|
||||||
|
if (_syncTitleAndFilename) {
|
||||||
|
_formKey.currentState?.fields[fkFileName]
|
||||||
|
?.didChange(transformedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
S.of(context)!.synchronizeTitleAndFilename,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Created at
|
||||||
|
FormBuilderDateTimePicker(
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
format: DateFormat.yMMMMd(),
|
||||||
|
inputType: InputType.date,
|
||||||
|
name: DocumentModel.createdKey,
|
||||||
|
initialValue: null,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() =>
|
||||||
|
_showDatePickerDeleteIcon = value != null);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.calendar_month_outlined),
|
||||||
|
labelText: S.of(context)!.createdAt + " *",
|
||||||
|
suffixIcon: _showDatePickerDeleteIcon
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
_formKey.currentState!
|
||||||
|
.fields[DocumentModel.createdKey]
|
||||||
|
?.didChange(null);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Correspondent
|
||||||
|
if (context
|
||||||
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canViewCorrespondents)
|
||||||
|
LabelFormField<Correspondent>(
|
||||||
|
showAnyAssignedOption: false,
|
||||||
|
showNotAssignedOption: false,
|
||||||
|
addLabelPageBuilder: (initialName) =>
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider.value(
|
||||||
|
value: context.read<LabelRepository>(),
|
||||||
|
),
|
||||||
|
Provider.value(
|
||||||
|
value: context.read<ApiVersion>(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: AddCorrespondentPage(
|
||||||
|
initialName: initialName),
|
||||||
|
),
|
||||||
|
addLabelText: S.of(context)!.addCorrespondent,
|
||||||
|
labelText: S.of(context)!.correspondent + " *",
|
||||||
|
name: DocumentModel.correspondentKey,
|
||||||
|
options: state.correspondents,
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
allowSelectUnassigned: true,
|
||||||
|
canCreateNewLabel: context
|
||||||
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canCreateCorrespondents,
|
||||||
|
),
|
||||||
|
// Document type
|
||||||
|
if (context
|
||||||
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canViewDocumentTypes)
|
||||||
|
LabelFormField<DocumentType>(
|
||||||
|
showAnyAssignedOption: false,
|
||||||
|
showNotAssignedOption: false,
|
||||||
|
addLabelPageBuilder: (initialName) =>
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider.value(
|
||||||
|
value: context.read<LabelRepository>(),
|
||||||
|
),
|
||||||
|
Provider.value(
|
||||||
|
value: context.read<ApiVersion>(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: AddDocumentTypePage(
|
||||||
|
initialName: initialName),
|
||||||
|
),
|
||||||
|
addLabelText: S.of(context)!.addDocumentType,
|
||||||
|
labelText: S.of(context)!.documentType + " *",
|
||||||
|
name: DocumentModel.documentTypeKey,
|
||||||
|
options: state.documentTypes,
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.description_outlined),
|
||||||
|
allowSelectUnassigned: true,
|
||||||
|
canCreateNewLabel: context
|
||||||
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canCreateDocumentTypes,
|
||||||
|
),
|
||||||
|
if (context
|
||||||
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canViewTags)
|
||||||
|
TagsFormField(
|
||||||
|
name: DocumentModel.tagsKey,
|
||||||
|
allowCreation: true,
|
||||||
|
allowExclude: false,
|
||||||
|
allowOnlySelection: true,
|
||||||
|
options: state.tags,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"* " + S.of(context)!.uploadInferValuesHint,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
textAlign: TextAlign.justify,
|
||||||
|
).padded(),
|
||||||
|
const SizedBox(height: 300),
|
||||||
|
].padded(),
|
||||||
),
|
),
|
||||||
Provider.value(
|
|
||||||
value: context.read<ApiVersion>(),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
child: AddDocumentTypePage(initialName: initialName),
|
);
|
||||||
),
|
},
|
||||||
addLabelText: S.of(context)!.addDocumentType,
|
|
||||||
labelText: S.of(context)!.documentType + " *",
|
|
||||||
name: DocumentModel.documentTypeKey,
|
|
||||||
options: state.documentTypes,
|
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
|
||||||
allowSelectUnassigned: true,
|
|
||||||
canCreateNewLabel: LocalUserAccount
|
|
||||||
.current.paperlessUser.canCreateDocumentTypes,
|
|
||||||
),
|
|
||||||
if (LocalUserAccount.current.paperlessUser.canViewTags)
|
|
||||||
TagsFormField(
|
|
||||||
name: DocumentModel.tagsKey,
|
|
||||||
allowCreation: true,
|
|
||||||
allowExclude: false,
|
|
||||||
allowOnlySelection: true,
|
|
||||||
options: state.tags,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"* " + S.of(context)!.uploadInferValuesHint,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 300),
|
),
|
||||||
].padded(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -289,14 +359,14 @@ class _DocumentUploadPreparationPageState
|
|||||||
?.whenOrNull(fromId: (id) => id);
|
?.whenOrNull(fromId: (id) => id);
|
||||||
final asn = fv[DocumentModel.asnKey] as int?;
|
final asn = fv[DocumentModel.asnKey] as int?;
|
||||||
final taskId = await cubit.upload(
|
final taskId = await cubit.upload(
|
||||||
widget.fileBytes,
|
await widget.fileBytes,
|
||||||
filename: _padWithExtension(
|
filename: _padWithExtension(
|
||||||
_formKey.currentState?.value[fkFileName],
|
_formKey.currentState?.value[fkFileName],
|
||||||
widget.fileExtension,
|
widget.fileExtension,
|
||||||
),
|
),
|
||||||
userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||||
.getValue()!
|
.getValue()!
|
||||||
.currentLoggedInUser!,
|
.loggedInUserId!,
|
||||||
title: title,
|
title: title,
|
||||||
documentType: docType,
|
documentType: docType,
|
||||||
correspondent: correspondent,
|
correspondent: correspondent,
|
||||||
@@ -308,10 +378,7 @@ class _DocumentUploadPreparationPageState
|
|||||||
context,
|
context,
|
||||||
S.of(context)!.documentSuccessfullyUploadedProcessing,
|
S.of(context)!.documentSuccessfullyUploadedProcessing,
|
||||||
);
|
);
|
||||||
Navigator.pop(
|
context.pop(DocumentUploadResult(true, taskId));
|
||||||
context,
|
|
||||||
DocumentUploadResult(true, taskId),
|
|
||||||
);
|
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} on PaperlessFormValidationException catch (exception) {
|
} on PaperlessFormValidationException catch (exception) {
|
||||||
@@ -336,4 +403,33 @@ class _DocumentUploadPreparationPageState
|
|||||||
String _formatFilename(String source) {
|
String _formatFilename(String source) {
|
||||||
return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase();
|
return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Future<Color> _computeAverageColor() async {
|
||||||
|
// final bitmap = img.decodeImage(await widget.fileBytes);
|
||||||
|
// if (bitmap == null) {
|
||||||
|
// return Colors.black;
|
||||||
|
// }
|
||||||
|
// int redBucket = 0;
|
||||||
|
// int greenBucket = 0;
|
||||||
|
// int blueBucket = 0;
|
||||||
|
// int pixelCount = 0;
|
||||||
|
|
||||||
|
// for (int y = 0; y < bitmap.height; y++) {
|
||||||
|
// for (int x = 0; x < bitmap.width; x++) {
|
||||||
|
// final c = bitmap.getPixel(x, y);
|
||||||
|
|
||||||
|
// pixelCount++;
|
||||||
|
// redBucket += c.r.toInt();
|
||||||
|
// greenBucket += c.g.toInt();
|
||||||
|
// blueBucket += c.b.toInt();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return Color.fromRGBO(
|
||||||
|
// redBucket ~/ pixelCount,
|
||||||
|
// greenBucket ~/ pixelCount,
|
||||||
|
// blueBucket ~/ pixelCount,
|
||||||
|
// 1,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
@@ -20,6 +21,8 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
@@ -31,6 +34,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
this.notifier,
|
this.notifier,
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this._userState,
|
this._userState,
|
||||||
|
this.connectivityStatusService,
|
||||||
) : super(DocumentsState(
|
) : super(DocumentsState(
|
||||||
filter: _userState.currentDocumentFilter,
|
filter: _userState.currentDocumentFilter,
|
||||||
viewType: _userState.documentsPageViewType,
|
viewType: _userState.documentsPageViewType,
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
import 'package:badges/badges.dart' as b;
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:defer_pointer/defer_pointer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
||||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_changed_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_views_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/confirm_delete_saved_view_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.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/labels/cubit/label_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_list.dart';
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
|
||||||
class DocumentFilterIntent {
|
class DocumentFilterIntent {
|
||||||
final DocumentFilter? filter;
|
final DocumentFilter? filter;
|
||||||
@@ -41,283 +44,260 @@ class DocumentsPage extends StatefulWidget {
|
|||||||
State<DocumentsPage> createState() => _DocumentsPageState();
|
State<DocumentsPage> createState() => _DocumentsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentsPageState extends State<DocumentsPage>
|
class _DocumentsPageState extends State<DocumentsPage> {
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
final SliverOverlapAbsorberHandle searchBarHandle =
|
||||||
SliverOverlapAbsorberHandle();
|
SliverOverlapAbsorberHandle();
|
||||||
final SliverOverlapAbsorberHandle tabBarHandle =
|
|
||||||
SliverOverlapAbsorberHandle();
|
|
||||||
late final TabController _tabController;
|
|
||||||
|
|
||||||
int _currentTab = 0;
|
final SliverOverlapAbsorberHandle savedViewsHandle =
|
||||||
|
SliverOverlapAbsorberHandle();
|
||||||
|
|
||||||
|
final _nestedScrollViewKey = GlobalKey<NestedScrollViewState>();
|
||||||
|
|
||||||
|
final _savedViewsExpansionController = ExpansionTileController();
|
||||||
|
bool _showExtendedFab = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final showSavedViews =
|
// context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
|
||||||
LocalUserAccount.current.paperlessUser.canViewSavedViews;
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_tabController = TabController(
|
_nestedScrollViewKey.currentState!.innerController
|
||||||
length: showSavedViews ? 2 : 1,
|
.addListener(_scrollExtentChangedListener);
|
||||||
vsync: this,
|
});
|
||||||
);
|
|
||||||
Future.wait([
|
|
||||||
context.read<DocumentsCubit>().reload(),
|
|
||||||
context.read<SavedViewCubit>().reload(),
|
|
||||||
]).onError<PaperlessApiException>(
|
|
||||||
(error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
_tabController.addListener(_tabChangesListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _tabChangesListener() {
|
void _onTasksChanged() {
|
||||||
setState(() => _currentTab = _tabController.index);
|
final notifier = context.read<PendingTasksNotifier>();
|
||||||
|
final tasks = notifier.value;
|
||||||
|
final finishedTasks = tasks.values.where((element) => element.isSuccess);
|
||||||
|
if (finishedTasks.isNotEmpty) {
|
||||||
|
showSnackBar(
|
||||||
|
context,
|
||||||
|
S.of(context)!.newDocumentAvailable,
|
||||||
|
action: SnackBarActionConfig(
|
||||||
|
label: S.of(context)!.reload,
|
||||||
|
onPressed: () {
|
||||||
|
// finishedTasks.forEach((task) {
|
||||||
|
// notifier.acknowledgeTasks([finishedTasks]);
|
||||||
|
// });
|
||||||
|
context.read<DocumentsCubit>().reload();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reloadData() async {
|
||||||
|
final user = context.read<LocalUserAccount>().paperlessUser;
|
||||||
|
try {
|
||||||
|
await Future.wait([
|
||||||
|
context.read<DocumentsCubit>().reload(),
|
||||||
|
if (user.canViewSavedViews) context.read<SavedViewCubit>().reload(),
|
||||||
|
if (user.canViewTags) context.read<LabelCubit>().reloadTags(),
|
||||||
|
if (user.canViewCorrespondents)
|
||||||
|
context.read<LabelCubit>().reloadCorrespondents(),
|
||||||
|
if (user.canViewDocumentTypes)
|
||||||
|
context.read<LabelCubit>().reloadDocumentTypes(),
|
||||||
|
if (user.canViewStoragePaths)
|
||||||
|
context.read<LabelCubit>().reloadStoragePaths(),
|
||||||
|
]);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
showGenericError(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollExtentChangedListener() {
|
||||||
|
const threshold = 400;
|
||||||
|
final offset =
|
||||||
|
_nestedScrollViewKey.currentState!.innerController.position.pixels;
|
||||||
|
if (offset < threshold && _showExtendedFab == false) {
|
||||||
|
setState(() {
|
||||||
|
_showExtendedFab = true;
|
||||||
|
});
|
||||||
|
} else if (offset >= threshold && _showExtendedFab == true) {
|
||||||
|
setState(() {
|
||||||
|
_showExtendedFab = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabController.dispose();
|
_nestedScrollViewKey.currentState?.innerController
|
||||||
|
.removeListener(_scrollExtentChangedListener);
|
||||||
|
// context.read<PendingTasksNotifier>().removeListener(_onTasksChanged);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<TaskStatusCubit, TaskStatusState>(
|
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
!previous.isSuccess && current.isSuccess,
|
previous != ConnectivityState.connected &&
|
||||||
|
current == ConnectivityState.connected,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
showSnackBar(
|
_reloadData();
|
||||||
context,
|
|
||||||
S.of(context)!.newDocumentAvailable,
|
|
||||||
action: SnackBarActionConfig(
|
|
||||||
label: S.of(context)!.reload,
|
|
||||||
onPressed: () {
|
|
||||||
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
|
|
||||||
context.read<DocumentsCubit>().reload();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
duration: const Duration(seconds: 10),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
builder: (context, connectivityState) {
|
||||||
listenWhen: (previous, current) =>
|
return SafeArea(
|
||||||
previous != ConnectivityState.connected &&
|
top: true,
|
||||||
current == ConnectivityState.connected,
|
child: Scaffold(
|
||||||
listener: (context, state) {
|
drawer: const AppDrawer(),
|
||||||
try {
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
context.read<DocumentsCubit>().reload();
|
builder: (context, state) {
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
final show = state.selection.isEmpty;
|
||||||
showErrorMessage(context, error, stackTrace);
|
final canReset = state.filter.appliedFiltersCount > 0;
|
||||||
}
|
if (show) {
|
||||||
},
|
return Column(
|
||||||
builder: (context, connectivityState) {
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
return SafeArea(
|
children: [
|
||||||
top: true,
|
DeferredPointerHandler(
|
||||||
child: Scaffold(
|
child: Stack(
|
||||||
drawer: const AppDrawer(),
|
clipBehavior: Clip.none,
|
||||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
children: [
|
||||||
builder: (context, state) {
|
FloatingActionButton.extended(
|
||||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
extendedPadding: _showExtendedFab
|
||||||
final show = state.selection.isEmpty;
|
? null
|
||||||
final canReset = state.filter.appliedFiltersCount > 0;
|
: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
return AnimatedScale(
|
heroTag: "fab_documents_page_filter",
|
||||||
scale: show ? 1 : 0,
|
label: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 150),
|
||||||
curve: Curves.easeIn,
|
transitionBuilder: (child, animation) {
|
||||||
child: Column(
|
return FadeTransition(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
opacity: animation,
|
||||||
children: [
|
child: SizeTransition(
|
||||||
if (canReset)
|
sizeFactor: animation,
|
||||||
Padding(
|
axis: Axis.horizontal,
|
||||||
padding: const EdgeInsets.all(8.0),
|
child: child,
|
||||||
child: FloatingActionButton.small(
|
),
|
||||||
key: UniqueKey(),
|
);
|
||||||
backgroundColor: Theme.of(context)
|
},
|
||||||
.colorScheme
|
child: _showExtendedFab
|
||||||
.onPrimaryContainer,
|
? Row(
|
||||||
onPressed: () {
|
children: [
|
||||||
context.read<DocumentsCubit>().updateFilter();
|
const Icon(
|
||||||
},
|
Icons.filter_alt_outlined,
|
||||||
child: Icon(
|
),
|
||||||
Icons.refresh,
|
const SizedBox(width: 8),
|
||||||
color: Theme.of(context)
|
Text(
|
||||||
.colorScheme
|
S.of(context)!.filterDocuments,
|
||||||
.primaryContainer,
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const Icon(Icons.filter_alt_outlined),
|
||||||
),
|
),
|
||||||
|
onPressed: _openDocumentFilter,
|
||||||
),
|
),
|
||||||
),
|
if (canReset)
|
||||||
b.Badge(
|
Positioned(
|
||||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
top: -20,
|
||||||
showBadge: appliedFiltersCount > 0,
|
right: -8,
|
||||||
badgeContent: Text(
|
child: DeferPointer(
|
||||||
'$appliedFiltersCount',
|
paintOnTop: true,
|
||||||
style: const TextStyle(
|
child: Material(
|
||||||
color: Colors.white,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
child: InkWell(
|
||||||
animationType: b.BadgeAnimationType.fade,
|
borderRadius: BorderRadius.circular(8),
|
||||||
badgeColor: Colors.red,
|
onTap: () {
|
||||||
child: AnimatedSwitcher(
|
HapticFeedback.mediumImpact();
|
||||||
duration: const Duration(milliseconds: 250),
|
_onResetFilter();
|
||||||
child: (_currentTab == 0)
|
},
|
||||||
? FloatingActionButton(
|
child: Row(
|
||||||
child:
|
mainAxisSize: MainAxisSize.min,
|
||||||
const Icon(Icons.filter_alt_outlined),
|
mainAxisAlignment:
|
||||||
onPressed: _openDocumentFilter,
|
MainAxisAlignment.spaceBetween,
|
||||||
)
|
children: [
|
||||||
: FloatingActionButton(
|
if (_showExtendedFab)
|
||||||
child: const Icon(Icons.add),
|
Text(
|
||||||
onPressed: () =>
|
"Reset (${state.filter.appliedFiltersCount})",
|
||||||
_onCreateSavedView(state.filter),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onError,
|
||||||
|
),
|
||||||
|
).padded()
|
||||||
|
else
|
||||||
|
Icon(
|
||||||
|
Icons.replay,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onError,
|
||||||
|
).padded(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
resizeToAvoidBottomInset: true,
|
|
||||||
body: WillPopScope(
|
|
||||||
onWillPop: () async {
|
|
||||||
if (context
|
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.state
|
|
||||||
.selection
|
|
||||||
.isNotEmpty) {
|
|
||||||
context.read<DocumentsCubit>().resetSelection();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
child: NestedScrollView(
|
|
||||||
floatHeaderSlivers: true,
|
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
|
||||||
SliverOverlapAbsorber(
|
|
||||||
handle: searchBarHandle,
|
|
||||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.selection.isEmpty) {
|
|
||||||
return SliverSearchBar(
|
|
||||||
floating: true,
|
|
||||||
titleText: S.of(context)!.documents,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return DocumentSelectionSliverAppBar(
|
|
||||||
state: state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverOverlapAbsorber(
|
|
||||||
handle: tabBarHandle,
|
|
||||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.selection.isNotEmpty) {
|
|
||||||
return const SliverToBoxAdapter(
|
|
||||||
child: SizedBox.shrink(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return SliverPersistentHeader(
|
|
||||||
pinned: true,
|
|
||||||
delegate:
|
|
||||||
CustomizableSliverPersistentHeaderDelegate(
|
|
||||||
minExtent: kTextTabBarHeight,
|
|
||||||
maxExtent: kTextTabBarHeight,
|
|
||||||
child: ColoredTabBar(
|
|
||||||
tabBar: TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
tabs: [
|
|
||||||
Tab(text: S.of(context)!.documents),
|
|
||||||
if (LocalUserAccount.current.paperlessUser
|
|
||||||
.canViewSavedViews)
|
|
||||||
Tab(text: S.of(context)!.views),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
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: TabBarView(
|
|
||||||
controller: _tabController,
|
|
||||||
physics: context
|
|
||||||
.watch<DocumentsCubit>()
|
|
||||||
.state
|
|
||||||
.selection
|
|
||||||
.isNotEmpty
|
|
||||||
? const NeverScrollableScrollPhysics()
|
|
||||||
: null,
|
|
||||||
children: [
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
return _buildDocumentsTab(
|
|
||||||
connectivityState,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
if (LocalUserAccount
|
),
|
||||||
.current.paperlessUser.canViewSavedViews)
|
],
|
||||||
Builder(
|
);
|
||||||
builder: (context) {
|
} else {
|
||||||
return _buildSavedViewsTab(
|
return const SizedBox.shrink();
|
||||||
connectivityState,
|
}
|
||||||
context,
|
},
|
||||||
);
|
),
|
||||||
},
|
resizeToAvoidBottomInset: true,
|
||||||
),
|
body: WillPopScope(
|
||||||
],
|
onWillPop: () async {
|
||||||
|
final cubit = context.read<DocumentsCubit>();
|
||||||
|
if (cubit.state.selection.isNotEmpty) {
|
||||||
|
cubit.resetSelection();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cubit.state.filter.appliedFiltersCount > 0 || cubit.state.filter.selectedView != null) {
|
||||||
|
await _onResetFilter();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
child: NestedScrollView(
|
||||||
|
key: _nestedScrollViewKey,
|
||||||
|
floatHeaderSlivers: true,
|
||||||
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
SliverOverlapAbsorber(
|
||||||
|
handle: searchBarHandle,
|
||||||
|
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.selection.isEmpty) {
|
||||||
|
return SliverSearchBar(
|
||||||
|
floating: true,
|
||||||
|
titleText: S.of(context)!.documents,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return DocumentSelectionSliverAppBar(
|
||||||
|
state: state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SliverOverlapAbsorber(
|
||||||
|
handle: savedViewsHandle,
|
||||||
|
sliver: SliverPinnedHeader(
|
||||||
|
child: Material(
|
||||||
|
child: _buildViewActions(),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: _buildDocumentsTab(
|
||||||
|
connectivityState,
|
||||||
|
context,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSavedViewsTab(
|
|
||||||
ConnectivityState connectivityState,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
return RefreshIndicator(
|
|
||||||
edgeOffset: kTextTabBarHeight,
|
|
||||||
onRefresh: _onReloadSavedViews,
|
|
||||||
notificationPredicate: (_) => connectivityState.isConnected,
|
|
||||||
child: CustomScrollView(
|
|
||||||
key: const PageStorageKey<String>("savedViews"),
|
|
||||||
slivers: <Widget>[
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: searchBarHandle,
|
|
||||||
),
|
),
|
||||||
SliverOverlapInjector(
|
);
|
||||||
handle: tabBarHandle,
|
},
|
||||||
),
|
|
||||||
const SavedViewList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,17 +309,19 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
// Listen for scroll notifications to load new data.
|
// Listen for scroll notifications to load new data.
|
||||||
// Scroll controller does not work here due to nestedscrollview limitations.
|
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||||
|
final offset = notification.metrics.pixels;
|
||||||
|
if (offset > 128 && _savedViewsExpansionController.isExpanded) {
|
||||||
|
_savedViewsExpansionController.collapse();
|
||||||
|
}
|
||||||
|
|
||||||
final currState = context.read<DocumentsCubit>().state;
|
|
||||||
final max = notification.metrics.maxScrollExtent;
|
final max = notification.metrics.maxScrollExtent;
|
||||||
|
final currentState = context.read<DocumentsCubit>().state;
|
||||||
if (max == 0 ||
|
if (max == 0 ||
|
||||||
_currentTab != 0 ||
|
currentState.isLoading ||
|
||||||
currState.isLoading ||
|
currentState.isLastPageLoaded) {
|
||||||
currState.isLastPageLoaded) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final offset = notification.metrics.pixels;
|
|
||||||
if (offset >= max * 0.7) {
|
if (offset >= max * 0.7) {
|
||||||
context
|
context
|
||||||
.read<DocumentsCubit>()
|
.read<DocumentsCubit>()
|
||||||
@@ -356,29 +338,77 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
edgeOffset: kTextTabBarHeight,
|
edgeOffset: kTextTabBarHeight + 2,
|
||||||
onRefresh: _onReloadDocuments,
|
onRefresh: _reloadData,
|
||||||
notificationPredicate: (_) => connectivityState.isConnected,
|
notificationPredicate: (_) => connectivityState.isConnected,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
key: const PageStorageKey<String>("documents"),
|
key: const PageStorageKey<String>("documents"),
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SliverOverlapInjector(handle: searchBarHandle),
|
SliverOverlapInjector(handle: searchBarHandle),
|
||||||
SliverOverlapInjector(handle: tabBarHandle),
|
SliverOverlapInjector(handle: savedViewsHandle),
|
||||||
_buildViewActions(),
|
SliverToBoxAdapter(
|
||||||
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.filter != current.filter,
|
||||||
|
builder: (context, state) {
|
||||||
|
final currentUser = context.watch<LocalUserAccount>();
|
||||||
|
if (!currentUser.paperlessUser.canViewSavedViews) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return SavedViewsWidget(
|
||||||
|
controller: _savedViewsExpansionController,
|
||||||
|
onViewSelected: (view) {
|
||||||
|
final cubit = context.read<DocumentsCubit>();
|
||||||
|
if (state.filter.selectedView == view.id) {
|
||||||
|
_onResetFilter();
|
||||||
|
} else {
|
||||||
|
cubit.updateFilter(
|
||||||
|
filter: view.toDocumentFilter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdateView: (view) async {
|
||||||
|
await context.read<SavedViewCubit>().update(view);
|
||||||
|
showSnackBar(
|
||||||
|
context, S.of(context)!.savedViewSuccessfullyUpdated);
|
||||||
|
},
|
||||||
|
onDeleteView: (view) async {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
final shouldRemove = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
ConfirmDeleteSavedViewDialog(view: view),
|
||||||
|
);
|
||||||
|
if (shouldRemove) {
|
||||||
|
final documentsCubit = context.read<DocumentsCubit>();
|
||||||
|
context.read<SavedViewCubit>().remove(view);
|
||||||
|
if (documentsCubit.state.filter.selectedView ==
|
||||||
|
view.id) {
|
||||||
|
documentsCubit.resetFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filter: state.filter,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.hasLoaded && state.documents.isEmpty) {
|
if (state.hasLoaded && state.documents.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: DocumentsEmptyState(
|
child: DocumentsEmptyState(
|
||||||
state: state,
|
state: state,
|
||||||
onReset: context.read<DocumentsCubit>().resetFilter,
|
onReset: _onResetFilter,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final allowToggleFilter = state.selection.isEmpty;
|
final allowToggleFilter = state.selection.isEmpty;
|
||||||
return SliverAdaptiveDocumentsView(
|
return SliverAdaptiveDocumentsView(
|
||||||
viewType: state.viewType,
|
viewType: state.viewType,
|
||||||
onTap: _openDetails,
|
onTap: (document) {
|
||||||
|
DocumentDetailsRoute($extra: document).push(context);
|
||||||
|
},
|
||||||
onSelected:
|
onSelected:
|
||||||
context.read<DocumentsCubit>().toggleDocumentSelection,
|
context.read<DocumentsCubit>().toggleDocumentSelection,
|
||||||
hasInternetConnection: connectivityState.isConnected,
|
hasInternetConnection: connectivityState.isConnected,
|
||||||
@@ -404,10 +434,12 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildViewActions() {
|
Widget _buildViewActions() {
|
||||||
return SliverToBoxAdapter(
|
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
builder: (context, state) {
|
||||||
builder: (context, state) {
|
return Container(
|
||||||
return Row(
|
padding: const EdgeInsets.all(4),
|
||||||
|
color: Theme.of(context).colorScheme.background,
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
SortDocumentsButton(
|
SortDocumentsButton(
|
||||||
@@ -418,23 +450,12 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
onChanged: context.read<DocumentsCubit>().setViewType,
|
onChanged: context.read<DocumentsCubit>().setViewType,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
).paddedSymmetrically(horizontal: 8, vertical: 4),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCreateSavedView(DocumentFilter filter) async {
|
|
||||||
final newView = await pushAddSavedViewRoute(context, filter: filter);
|
|
||||||
if (newView != null) {
|
|
||||||
try {
|
|
||||||
await context.read<SavedViewCubit>().add(newView);
|
|
||||||
} on PaperlessApiException 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>(
|
||||||
@@ -476,7 +497,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
if (filterIntent != null) {
|
if (filterIntent != null) {
|
||||||
try {
|
try {
|
||||||
if (filterIntent.shouldReset) {
|
if (filterIntent.shouldReset) {
|
||||||
await context.read<DocumentsCubit>().resetFilter();
|
await _onResetFilter();
|
||||||
} else {
|
} else {
|
||||||
await context
|
await context
|
||||||
.read<DocumentsCubit>()
|
.read<DocumentsCubit>()
|
||||||
@@ -488,13 +509,6 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openDetails(DocumentModel document) {
|
|
||||||
pushDocumentDetailsRoute(
|
|
||||||
context,
|
|
||||||
document: document,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addTagToFilter(int tagId) {
|
void _addTagToFilter(int tagId) {
|
||||||
final cubit = context.read<DocumentsCubit>();
|
final cubit = context.read<DocumentsCubit>();
|
||||||
try {
|
try {
|
||||||
@@ -632,21 +646,46 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onReloadDocuments() async {
|
///
|
||||||
try {
|
/// Resets the current filter and scrolls all the way to the top of the view.
|
||||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
/// If a saved view is currently selected and the filter has changed,
|
||||||
await context.read<DocumentsCubit>().reload();
|
/// the user will be shown a dialog informing them about the changes.
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
/// The user can then decide whether to abort the reset or to continue and discard the changes.
|
||||||
showErrorMessage(context, error, stackTrace);
|
Future<void> _onResetFilter() async {
|
||||||
}
|
final cubit = context.read<DocumentsCubit>();
|
||||||
}
|
final savedViewCubit = context.read<SavedViewCubit>();
|
||||||
|
|
||||||
Future<void> _onReloadSavedViews() async {
|
void toTop() async {
|
||||||
try {
|
await _nestedScrollViewKey.currentState?.outerController.animateTo(
|
||||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
0,
|
||||||
await context.read<SavedViewCubit>().reload();
|
duration: const Duration(milliseconds: 300),
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
curve: Curves.easeOut,
|
||||||
showErrorMessage(context, error, stackTrace);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final activeView = savedViewCubit.state.mapOrNull(
|
||||||
|
loaded: (state) {
|
||||||
|
if (cubit.state.filter.selectedView != null) {
|
||||||
|
return state.savedViews[cubit.state.filter.selectedView!];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final viewHasChanged = activeView != null &&
|
||||||
|
activeView.toDocumentFilter() != cubit.state.filter;
|
||||||
|
if (viewHasChanged) {
|
||||||
|
final discardChanges = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const SavedViewChangedDialog(),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (discardChanges) {
|
||||||
|
cubit.resetFilter();
|
||||||
|
toTop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cubit.resetFilter();
|
||||||
|
toTop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ class DocumentPreview extends StatelessWidget {
|
|||||||
final double borderRadius;
|
final double borderRadius;
|
||||||
final bool enableHero;
|
final bool enableHero;
|
||||||
final double scale;
|
final double scale;
|
||||||
|
final bool isClickable;
|
||||||
|
|
||||||
const DocumentPreview({
|
const DocumentPreview({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -21,15 +24,26 @@ class DocumentPreview extends StatelessWidget {
|
|||||||
this.borderRadius = 12.0,
|
this.borderRadius = 12.0,
|
||||||
this.enableHero = true,
|
this.enableHero = true,
|
||||||
this.scale = 1.1,
|
this.scale = 1.1,
|
||||||
|
this.isClickable = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return HeroMode(
|
return ConnectivityAwareActionWrapper(
|
||||||
enabled: enableHero,
|
child: GestureDetector(
|
||||||
child: Hero(
|
behavior: HitTestBehavior.translucent,
|
||||||
tag: "thumb_${document.id}",
|
onTap: isClickable
|
||||||
child: _buildPreview(context),
|
? () => DocumentPreviewRoute($extra: document).push(context)
|
||||||
|
: null,
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
if (enableHero) {
|
||||||
|
return Hero(
|
||||||
|
tag: "thumb_${document.id}",
|
||||||
|
child: _buildPreview(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _buildPreview(context);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -8,6 +8,7 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|||||||
class DocumentsEmptyState extends StatelessWidget {
|
class DocumentsEmptyState extends StatelessWidget {
|
||||||
final DocumentPagingState state;
|
final DocumentPagingState state;
|
||||||
final VoidCallback? onReset;
|
final VoidCallback? onReset;
|
||||||
|
|
||||||
const DocumentsEmptyState({
|
const DocumentsEmptyState({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.state,
|
required this.state,
|
||||||
@@ -17,18 +18,24 @@ class DocumentsEmptyState extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return Center(
|
||||||
child: EmptyState(
|
child: Column(
|
||||||
title: S.of(context)!.oops,
|
children: [
|
||||||
subtitle: S.of(context)!.thereSeemsToBeNothingHere,
|
Text(
|
||||||
bottomChild: state.filter != DocumentFilter.initial && onReset != null
|
S.of(context)!.noDocumentsFound,
|
||||||
? TextButton(
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
onPressed: onReset,
|
),
|
||||||
child: Text(
|
if (state.filter != DocumentFilter.initial && onReset != null)
|
||||||
S.of(context)!.resetFilter,
|
TextButton(
|
||||||
),
|
onPressed: () {
|
||||||
).padded()
|
HapticFeedback.mediumImpact();
|
||||||
: null,
|
onReset!();
|
||||||
),
|
},
|
||||||
|
child: Text(
|
||||||
|
S.of(context)!.resetFilter,
|
||||||
|
),
|
||||||
|
).padded(),
|
||||||
|
],
|
||||||
|
).padded(24),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.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/document_preview.dart';
|
||||||
@@ -32,6 +37,12 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final currentUserId = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||||
|
.getValue()!
|
||||||
|
.loggedInUserId;
|
||||||
|
final paperlessUser = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||||
|
.get(currentUserId)!
|
||||||
|
.paperlessUser;
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final insets = MediaQuery.of(context).viewInsets;
|
final insets = MediaQuery.of(context).viewInsets;
|
||||||
final padding = MediaQuery.of(context).viewPadding;
|
final padding = MediaQuery.of(context).viewPadding;
|
||||||
@@ -104,48 +115,51 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).paddedLTRB(8, 0, 8, 4),
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
Row(
|
if (paperlessUser.canViewCorrespondents)
|
||||||
children: [
|
Row(
|
||||||
const Icon(
|
children: [
|
||||||
Icons.person_outline,
|
const Icon(
|
||||||
size: 16,
|
Icons.person_outline,
|
||||||
).paddedOnly(right: 4.0),
|
size: 16,
|
||||||
CorrespondentWidget(
|
).paddedOnly(right: 4.0),
|
||||||
onSelected: onCorrespondentSelected,
|
CorrespondentWidget(
|
||||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
onSelected: onCorrespondentSelected,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||||
),
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
correspondent: context
|
),
|
||||||
.watch<LabelRepository>()
|
correspondent: context
|
||||||
.state
|
.watch<LabelRepository>()
|
||||||
.correspondents[document.correspondent],
|
.state
|
||||||
),
|
.correspondents[document.correspondent],
|
||||||
],
|
),
|
||||||
).paddedLTRB(8, 0, 8, 4),
|
],
|
||||||
Row(
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
children: [
|
if (paperlessUser.canViewDocumentTypes)
|
||||||
const Icon(
|
Row(
|
||||||
Icons.description_outlined,
|
children: [
|
||||||
size: 16,
|
const Icon(
|
||||||
).paddedOnly(right: 4.0),
|
Icons.description_outlined,
|
||||||
DocumentTypeWidget(
|
size: 16,
|
||||||
onSelected: onDocumentTypeSelected,
|
).paddedOnly(right: 4.0),
|
||||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
DocumentTypeWidget(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
onSelected: onDocumentTypeSelected,
|
||||||
),
|
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||||
documentType: context
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
.watch<LabelRepository>()
|
),
|
||||||
.state
|
documentType: context
|
||||||
.documentTypes[document.documentType],
|
.watch<LabelRepository>()
|
||||||
),
|
.state
|
||||||
],
|
.documentTypes[document.documentType],
|
||||||
).paddedLTRB(8, 0, 8, 4),
|
),
|
||||||
TagsWidget(
|
],
|
||||||
tags: document.tags
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
|
if (paperlessUser.canViewTags)
|
||||||
.toList(),
|
TagsWidget(
|
||||||
onTagSelected: onTagSelected,
|
tags: document.tags
|
||||||
).padded(),
|
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
|
||||||
|
.toList(),
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
).padded(),
|
||||||
if (highlights != null)
|
if (highlights != null)
|
||||||
Html(
|
Html(
|
||||||
data: '<p>${highlights!}</p>',
|
data: '<p>${highlights!}</p>',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||||
@@ -26,6 +28,7 @@ class DocumentGridItem extends DocumentItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -64,15 +67,16 @@ class DocumentGridItem extends DocumentItem {
|
|||||||
const SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
child: SizedBox(width: 8),
|
child: SizedBox(width: 8),
|
||||||
),
|
),
|
||||||
TagsWidget.sliver(
|
if (currentUser.canViewTags)
|
||||||
tags: document.tags
|
TagsWidget.sliver(
|
||||||
.map((e) => context
|
tags: document.tags
|
||||||
.watch<LabelRepository>()
|
.map((e) => context
|
||||||
.state
|
.watch<LabelRepository>()
|
||||||
.tags[e]!)
|
.state
|
||||||
.toList(),
|
.tags[e]!)
|
||||||
onTagSelected: onTagSelected,
|
.toList(),
|
||||||
),
|
onTagSelected: onTagSelected,
|
||||||
|
),
|
||||||
const SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
child: SizedBox(width: 8),
|
child: SizedBox(width: 8),
|
||||||
),
|
),
|
||||||
@@ -90,20 +94,22 @@ class DocumentGridItem extends DocumentItem {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CorrespondentWidget(
|
if (currentUser.canViewCorrespondents)
|
||||||
correspondent: context
|
CorrespondentWidget(
|
||||||
.watch<LabelRepository>()
|
correspondent: context
|
||||||
.state
|
.watch<LabelRepository>()
|
||||||
.correspondents[document.correspondent],
|
.state
|
||||||
onSelected: onCorrespondentSelected,
|
.correspondents[document.correspondent],
|
||||||
),
|
onSelected: onCorrespondentSelected,
|
||||||
DocumentTypeWidget(
|
),
|
||||||
documentType: context
|
if (currentUser.canViewDocumentTypes)
|
||||||
.watch<LabelRepository>()
|
DocumentTypeWidget(
|
||||||
.state
|
documentType: context
|
||||||
.documentTypes[document.documentType],
|
.watch<LabelRepository>()
|
||||||
onSelected: onDocumentTypeSelected,
|
.state
|
||||||
),
|
.documentTypes[document.documentType],
|
||||||
|
onSelected: onDocumentTypeSelected,
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import 'package:provider/provider.dart';
|
|||||||
class DocumentListItem extends DocumentItem {
|
class DocumentListItem extends DocumentItem {
|
||||||
static const _a4AspectRatio = 1 / 1.4142;
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
|
|
||||||
|
final Color? backgroundColor;
|
||||||
const DocumentListItem({
|
const DocumentListItem({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.backgroundColor,
|
||||||
required super.document,
|
required super.document,
|
||||||
required super.isSelected,
|
required super.isSelected,
|
||||||
required super.isSelectionActive,
|
required super.isSelectionActive,
|
||||||
@@ -29,91 +31,90 @@ class DocumentListItem extends DocumentItem {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final labels = context.watch<LabelRepository>().state;
|
final labels = context.watch<LabelRepository>().state;
|
||||||
return Material(
|
return ListTile(
|
||||||
child: ListTile(
|
tileColor: backgroundColor,
|
||||||
dense: true,
|
dense: true,
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
onTap: () => _onTap(),
|
onTap: () => _onTap(),
|
||||||
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
|
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
|
||||||
onLongPress: () => onSelected?.call(document),
|
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: isSelectionActive,
|
absorbing: isSelectionActive,
|
||||||
child: CorrespondentWidget(
|
child: CorrespondentWidget(
|
||||||
isClickable: isLabelClickable,
|
isClickable: isLabelClickable,
|
||||||
correspondent: context
|
correspondent: context
|
||||||
.watch<LabelRepository>()
|
.watch<LabelRepository>()
|
||||||
.state
|
.state
|
||||||
.correspondents[document.correspondent],
|
.correspondents[document.correspondent],
|
||||||
onSelected: onCorrespondentSelected,
|
onSelected: onCorrespondentSelected,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
document.title,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
AbsorbPointer(
|
|
||||||
absorbing: isSelectionActive,
|
|
||||||
child: TagsWidget(
|
|
||||||
isClickable: isLabelClickable,
|
|
||||||
tags: document.tags
|
|
||||||
.where((e) => labels.tags.containsKey(e))
|
|
||||||
.map((e) => labels.tags[e]!)
|
|
||||||
.toList(),
|
|
||||||
onTagSelected: (id) => onTagSelected?.call(id),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
Text(
|
||||||
subtitle: Padding(
|
document.title,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: RichText(
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
text: TextSpan(
|
maxLines: 1,
|
||||||
text: DateFormat.yMMMd().format(document.created),
|
),
|
||||||
style: Theme.of(context)
|
AbsorbPointer(
|
||||||
.textTheme
|
absorbing: isSelectionActive,
|
||||||
.labelSmall
|
child: TagsWidget(
|
||||||
?.apply(color: Colors.grey),
|
isClickable: isLabelClickable,
|
||||||
children: document.documentType != null
|
tags: document.tags
|
||||||
? [
|
.where((e) => labels.tags.containsKey(e))
|
||||||
const TextSpan(text: '\u30FB'),
|
.map((e) => labels.tags[e]!)
|
||||||
TextSpan(
|
.toList(),
|
||||||
text: labels.documentTypes[document.documentType]?.name,
|
onTagSelected: (id) => onTagSelected?.call(id),
|
||||||
recognizer: onDocumentTypeSelected != null
|
|
||||||
? (TapGestureRecognizer()
|
|
||||||
..onTap = () => onDocumentTypeSelected!(
|
|
||||||
document.documentType))
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
isThreeLine: document.tags.isNotEmpty,
|
|
||||||
leading: AspectRatio(
|
|
||||||
aspectRatio: _a4AspectRatio,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: DocumentPreview(
|
|
||||||
document: document,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
enableHero: enableHeroAnimation,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.all(8.0),
|
|
||||||
),
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: 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: labels.documentTypes[document.documentType]?.name,
|
||||||
|
recognizer: onDocumentTypeSelected != null
|
||||||
|
? (TapGestureRecognizer()
|
||||||
|
..onTap = () =>
|
||||||
|
onDocumentTypeSelected!(document.documentType))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isThreeLine: document.tags.isNotEmpty,
|
||||||
|
leading: AspectRatio(
|
||||||
|
aspectRatio: _a4AspectRatio,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: DocumentPreview(
|
||||||
|
document: document,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
enableHero: enableHeroAnimation,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.all(8.0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
|
|
||||||
class NewItemsLoadingWidget extends StatelessWidget {
|
|
||||||
const NewItemsLoadingWidget({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Center(child: const CircularProgressIndicator().padded());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class SavedViewChangedDialog extends StatelessWidget {
|
||||||
|
const SavedViewChangedDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(S.of(context)!.discardChanges),
|
||||||
|
content: Text(S.of(context)!.savedViewChangedDialogContent),
|
||||||
|
actionsOverflowButtonSpacing: 8,
|
||||||
|
actions: [
|
||||||
|
const DialogCancelButton(),
|
||||||
|
DialogConfirmButton(
|
||||||
|
label: S.of(context)!.resetFilter,
|
||||||
|
style: DialogConfirmButtonStyle.danger,
|
||||||
|
returnValue: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||||
|
|
||||||
|
class SavedViewChip extends StatefulWidget {
|
||||||
|
final SavedView view;
|
||||||
|
final void Function(SavedView view) onViewSelected;
|
||||||
|
final void Function(SavedView view) onUpdateView;
|
||||||
|
final void Function(SavedView view) onDeleteView;
|
||||||
|
final bool selected;
|
||||||
|
final bool hasChanged;
|
||||||
|
|
||||||
|
const SavedViewChip({
|
||||||
|
super.key,
|
||||||
|
required this.view,
|
||||||
|
required this.onViewSelected,
|
||||||
|
required this.selected,
|
||||||
|
required this.hasChanged,
|
||||||
|
required this.onUpdateView,
|
||||||
|
required this.onDeleteView,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SavedViewChip> createState() => _SavedViewChipState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SavedViewChipState extends State<SavedViewChip>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
_animation = _animationController.drive(Tween(begin: 0, end: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isExpanded = false;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final effectiveBackgroundColor = widget.selected
|
||||||
|
? colorScheme.secondaryContainer
|
||||||
|
: colorScheme.surfaceVariant;
|
||||||
|
final effectiveForegroundColor = widget.selected
|
||||||
|
? colorScheme.onSecondaryContainer
|
||||||
|
: colorScheme.onSurfaceVariant;
|
||||||
|
|
||||||
|
final expandedChild = Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: effectiveForegroundColor,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
EditSavedViewRoute(widget.view).push(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: colorScheme.error,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
widget.onDeleteView(widget.view);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: effectiveBackgroundColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: BorderSide(
|
||||||
|
color: colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
enableFeedback: true,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: () => widget.onViewSelected(widget.view),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildCheckmark(effectiveForegroundColor),
|
||||||
|
_buildLabel(context, effectiveForegroundColor)
|
||||||
|
.paddedSymmetrically(
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddedOnly(left: 8),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
child: _isExpanded ? expandedChild : const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
_buildTrailing(effectiveForegroundColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailing(Color effectiveForegroundColor) {
|
||||||
|
return IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: _animation.value * pi,
|
||||||
|
child: Icon(
|
||||||
|
_isExpanded ? Icons.close : Icons.chevron_right,
|
||||||
|
color: effectiveForegroundColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (_isExpanded) {
|
||||||
|
_animationController.reverse();
|
||||||
|
} else {
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isExpanded = !_isExpanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLabel(BuildContext context, Color effectiveForegroundColor) {
|
||||||
|
return Text(
|
||||||
|
widget.view.name,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(color: effectiveForegroundColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCheckmark(Color effectiveForegroundColor) {
|
||||||
|
return AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: widget.selected
|
||||||
|
? Icon(Icons.check, color: effectiveForegroundColor)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
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/shimmer_placeholder.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_chip.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||||
|
|
||||||
|
class SavedViewsWidget extends StatefulWidget {
|
||||||
|
final void Function(SavedView view) onViewSelected;
|
||||||
|
final void Function(SavedView view) onUpdateView;
|
||||||
|
final void Function(SavedView view) onDeleteView;
|
||||||
|
|
||||||
|
final DocumentFilter filter;
|
||||||
|
final ExpansionTileController? controller;
|
||||||
|
|
||||||
|
const SavedViewsWidget({
|
||||||
|
super.key,
|
||||||
|
required this.onViewSelected,
|
||||||
|
required this.filter,
|
||||||
|
required this.onUpdateView,
|
||||||
|
required this.onDeleteView,
|
||||||
|
this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SavedViewsWidget> createState() => _SavedViewsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SavedViewsWidgetState extends State<SavedViewsWidget>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _animationController;
|
||||||
|
late final Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
_animation = _animationController.drive(Tween(begin: 0, end: 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final selectedView = state.mapOrNull(
|
||||||
|
loaded: (value) {
|
||||||
|
if (widget.filter.selectedView != null) {
|
||||||
|
return value.savedViews[widget.filter.selectedView!];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final selectedViewHasChanged = selectedView != null &&
|
||||||
|
selectedView.toDocumentFilter() != widget.filter;
|
||||||
|
return PageStorage(
|
||||||
|
bucket: PageStorageBucket(),
|
||||||
|
child: ExpansionTile(
|
||||||
|
controller: widget.controller,
|
||||||
|
tilePadding: const EdgeInsets.only(left: 8),
|
||||||
|
trailing: RotationTransition(
|
||||||
|
turns: _animation,
|
||||||
|
child: const Icon(Icons.expand_more),
|
||||||
|
).paddedOnly(right: 8),
|
||||||
|
onExpansionChanged: (isExpanded) {
|
||||||
|
if (isExpanded) {
|
||||||
|
_animationController.forward();
|
||||||
|
} else {
|
||||||
|
_animationController.reverse().then((value) => setState(() {}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
S.of(context)!.views,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
if (selectedView != null)
|
||||||
|
Text(
|
||||||
|
selectedView.name,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onBackground
|
||||||
|
.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedScale(
|
||||||
|
scale: selectedViewHasChanged ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final newView = selectedView!.copyWith(
|
||||||
|
filterRules: FilterRule.fromFilter(widget.filter),
|
||||||
|
);
|
||||||
|
widget.onUpdateView(newView);
|
||||||
|
},
|
||||||
|
child: Text(S.of(context)!.saveChanges),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
Icons.saved_search,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
).padded(),
|
||||||
|
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
state
|
||||||
|
.maybeMap(
|
||||||
|
loaded: (value) {
|
||||||
|
if (value.savedViews.isEmpty) {
|
||||||
|
return Text(S.of(context)!.youDidNotSaveAnyViewsYet)
|
||||||
|
.paddedOnly(left: 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: kMinInteractiveDimension,
|
||||||
|
child: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (notification) => true,
|
||||||
|
child: CustomScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
slivers: [
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(width: 12),
|
||||||
|
),
|
||||||
|
SliverList.separated(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final view =
|
||||||
|
value.savedViews.values.elementAt(index);
|
||||||
|
final isSelected =
|
||||||
|
(widget.filter.selectedView ?? -1) ==
|
||||||
|
view.id;
|
||||||
|
return ConnectivityAwareActionWrapper(
|
||||||
|
child: SavedViewChip(
|
||||||
|
view: view,
|
||||||
|
onViewSelected: widget.onViewSelected,
|
||||||
|
selected: isSelected,
|
||||||
|
hasChanged: isSelected &&
|
||||||
|
view.toDocumentFilter() !=
|
||||||
|
widget.filter,
|
||||||
|
onUpdateView: widget.onUpdateView,
|
||||||
|
onDeleteView: widget.onDeleteView,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) =>
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
itemCount: value.savedViews.length,
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(width: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (_) => Text(S.of(context)!.couldNotLoadSavedViews)
|
||||||
|
.paddedOnly(left: 16),
|
||||||
|
orElse: _buildLoadingState,
|
||||||
|
)
|
||||||
|
.paddedOnly(top: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Tooltip(
|
||||||
|
message: S.of(context)!.createFromCurrentFilter,
|
||||||
|
child: ConnectivityAwareActionWrapper(
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
CreateSavedViewRoute(widget.filter).push(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(S.of(context)!.newView),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padded(4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingState() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(top: 16),
|
||||||
|
height: kMinInteractiveDimension,
|
||||||
|
child: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (notification) => true,
|
||||||
|
child: ShimmerPlaceholder(
|
||||||
|
child: CustomScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
slivers: [
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(width: 12),
|
||||||
|
),
|
||||||
|
SliverList.separated(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Container(
|
||||||
|
width: 130,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(width: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
@@ -18,10 +19,12 @@ class DocumentFilterForm extends StatefulWidget {
|
|||||||
static const fkAddedAt = DocumentModel.addedKey;
|
static const fkAddedAt = DocumentModel.addedKey;
|
||||||
|
|
||||||
static DocumentFilter assembleFilter(
|
static DocumentFilter assembleFilter(
|
||||||
GlobalKey<FormBuilderState> formKey, DocumentFilter initialFilter) {
|
GlobalKey<FormBuilderState> formKey,
|
||||||
|
DocumentFilter initialFilter,
|
||||||
|
) {
|
||||||
formKey.currentState?.save();
|
formKey.currentState?.save();
|
||||||
final v = formKey.currentState!.value;
|
final v = formKey.currentState!.value;
|
||||||
return DocumentFilter(
|
return initialFilter.copyWith(
|
||||||
correspondent:
|
correspondent:
|
||||||
v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
|
v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
|
||||||
DocumentFilter.initial.correspondent,
|
DocumentFilter.initial.correspondent,
|
||||||
@@ -35,11 +38,7 @@ class DocumentFilterForm extends StatefulWidget {
|
|||||||
DocumentFilter.initial.query,
|
DocumentFilter.initial.query,
|
||||||
created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery),
|
created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery),
|
||||||
added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery),
|
added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery),
|
||||||
asnQuery: initialFilter.asnQuery,
|
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: initialFilter.pageSize,
|
|
||||||
sortField: initialFilter.sortField,
|
|
||||||
sortOrder: initialFilter.sortOrder,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +159,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
initialValue: widget.initialFilter.documentType,
|
initialValue: widget.initialFilter.documentType,
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
allowSelectUnassigned: false,
|
allowSelectUnassigned: false,
|
||||||
canCreateNewLabel:
|
canCreateNewLabel: context
|
||||||
LocalUserAccount.current.paperlessUser.canCreateDocumentTypes,
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canCreateDocumentTypes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,8 +174,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
initialValue: widget.initialFilter.correspondent,
|
initialValue: widget.initialFilter.correspondent,
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
allowSelectUnassigned: false,
|
allowSelectUnassigned: false,
|
||||||
canCreateNewLabel:
|
canCreateNewLabel: context
|
||||||
LocalUserAccount.current.paperlessUser.canCreateCorrespondents,
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canCreateCorrespondents,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +190,7 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
prefixIcon: const Icon(Icons.folder_outlined),
|
||||||
allowSelectUnassigned: false,
|
allowSelectUnassigned: false,
|
||||||
canCreateNewLabel:
|
canCreateNewLabel:
|
||||||
LocalUserAccount.current.paperlessUser.canCreateStoragePaths,
|
context.watch<LocalUserAccount>().paperlessUser.canCreateStoragePaths,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
|
heroTag: "fab_document_filter_panel",
|
||||||
icon: const Icon(Icons.done),
|
icon: const Icon(Icons.done),
|
||||||
label: Text(S.of(context)!.apply),
|
label: Text(S.of(context)!.apply),
|
||||||
onPressed: _onApplyFilter,
|
onPressed: _onApplyFilter,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
S.of(context)!.deleteView + view.name + "?",
|
S.of(context)!.deleteView(view.name),
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
),
|
||||||
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),
|
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),
|
||||||
|
|||||||
+17
-8
@@ -1,12 +1,12 @@
|
|||||||
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/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.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/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
|
||||||
class DocumentSelectionSliverAppBar extends StatelessWidget {
|
class DocumentSelectionSliverAppBar extends StatelessWidget {
|
||||||
final DocumentsState state;
|
final DocumentsState state;
|
||||||
@@ -65,24 +65,30 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
label: Text(S.of(context)!.correspondent),
|
label: Text(S.of(context)!.correspondent),
|
||||||
avatar: const Icon(Icons.edit),
|
avatar: const Icon(Icons.edit),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
pushBulkEditCorrespondentRoute(context,
|
BulkEditDocumentsRoute(BulkEditExtraWrapper(
|
||||||
selection: state.selection);
|
state.selection,
|
||||||
|
LabelType.correspondent,
|
||||||
|
)).push(context);
|
||||||
},
|
},
|
||||||
).paddedOnly(left: 8, right: 4),
|
).paddedOnly(left: 8, right: 4),
|
||||||
ActionChip(
|
ActionChip(
|
||||||
label: Text(S.of(context)!.documentType),
|
label: Text(S.of(context)!.documentType),
|
||||||
avatar: const Icon(Icons.edit),
|
avatar: const Icon(Icons.edit),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
pushBulkEditDocumentTypeRoute(context,
|
BulkEditDocumentsRoute(BulkEditExtraWrapper(
|
||||||
selection: state.selection);
|
state.selection,
|
||||||
|
LabelType.documentType,
|
||||||
|
)).push(context);
|
||||||
},
|
},
|
||||||
).paddedOnly(left: 8, right: 4),
|
).paddedOnly(left: 8, right: 4),
|
||||||
ActionChip(
|
ActionChip(
|
||||||
label: Text(S.of(context)!.storagePath),
|
label: Text(S.of(context)!.storagePath),
|
||||||
avatar: const Icon(Icons.edit),
|
avatar: const Icon(Icons.edit),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
pushBulkEditStoragePathRoute(context,
|
BulkEditDocumentsRoute(BulkEditExtraWrapper(
|
||||||
selection: state.selection);
|
state.selection,
|
||||||
|
LabelType.storagePath,
|
||||||
|
)).push(context);
|
||||||
},
|
},
|
||||||
).paddedOnly(left: 8, right: 4),
|
).paddedOnly(left: 8, right: 4),
|
||||||
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),
|
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),
|
||||||
@@ -98,7 +104,10 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
label: Text(S.of(context)!.tags),
|
label: Text(S.of(context)!.tags),
|
||||||
avatar: const Icon(Icons.edit),
|
avatar: const Icon(Icons.edit),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
pushBulkEditTagsRoute(context, selection: state.selection);
|
BulkEditDocumentsRoute(BulkEditExtraWrapper(
|
||||||
|
state.selection,
|
||||||
|
LabelType.tag,
|
||||||
|
)).push(context);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:paperless_mobile/core/translation/sort_field_localization_mapper
|
|||||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.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/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
|
|
||||||
class SortDocumentsButton extends StatelessWidget {
|
class SortDocumentsButton extends StatelessWidget {
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
@@ -20,55 +21,65 @@ class SortDocumentsButton extends StatelessWidget {
|
|||||||
if (state.filter.sortField == null) {
|
if (state.filter.sortField == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
print(state.filter.sortField);
|
final icon = Icon(state.filter.sortOrder == SortOrder.ascending
|
||||||
return TextButton.icon(
|
? Icons.arrow_upward
|
||||||
icon: Icon(state.filter.sortOrder == SortOrder.ascending
|
: Icons.arrow_downward);
|
||||||
? Icons.arrow_upward
|
final label = Text(translateSortField(context, state.filter.sortField));
|
||||||
: Icons.arrow_downward),
|
return ConnectivityAwareActionWrapper(
|
||||||
label: Text(translateSortField(context, state.filter.sortField)),
|
offlineBuilder: (context, child) {
|
||||||
onPressed: enabled
|
return TextButton.icon(
|
||||||
? () {
|
icon: icon,
|
||||||
showModalBottomSheet(
|
label: label,
|
||||||
elevation: 2,
|
onPressed: null,
|
||||||
context: context,
|
);
|
||||||
isScrollControlled: true,
|
},
|
||||||
shape: const RoundedRectangleBorder(
|
child: TextButton.icon(
|
||||||
borderRadius: BorderRadius.only(
|
icon: icon,
|
||||||
topLeft: Radius.circular(16),
|
label: label,
|
||||||
topRight: Radius.circular(16),
|
onPressed: enabled
|
||||||
),
|
? () {
|
||||||
),
|
showModalBottomSheet(
|
||||||
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
elevation: 2,
|
||||||
value: context.read<DocumentsCubit>(),
|
context: context,
|
||||||
child: MultiBlocProvider(
|
isScrollControlled: true,
|
||||||
providers: [
|
shape: const RoundedRectangleBorder(
|
||||||
BlocProvider(
|
borderRadius: BorderRadius.only(
|
||||||
create: (context) => LabelCubit(context.read()),
|
topLeft: Radius.circular(16),
|
||||||
),
|
topRight: Radius.circular(16),
|
||||||
],
|
|
||||||
child: SortFieldSelectionBottomSheet(
|
|
||||||
initialSortField: state.filter.sortField,
|
|
||||||
initialSortOrder: state.filter.sortOrder,
|
|
||||||
onSubmit: (field, order) {
|
|
||||||
return context
|
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.updateCurrentFilter(
|
|
||||||
(filter) => filter.copyWith(
|
|
||||||
sortField: field,
|
|
||||||
sortOrder: order,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
correspondents: state.correspondents,
|
|
||||||
documentTypes: state.documentTypes,
|
|
||||||
storagePaths: state.storagePaths,
|
|
||||||
tags: state.tags,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
||||||
);
|
value: context.read<DocumentsCubit>(),
|
||||||
}
|
child: MultiBlocProvider(
|
||||||
: null,
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => LabelCubit(context.read()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: SortFieldSelectionBottomSheet(
|
||||||
|
initialSortField: state.filter.sortField,
|
||||||
|
initialSortOrder: state.filter.sortOrder,
|
||||||
|
onSubmit: (field, order) {
|
||||||
|
return context
|
||||||
|
.read<DocumentsCubit>()
|
||||||
|
.updateCurrentFilter(
|
||||||
|
(filter) => filter.copyWith(
|
||||||
|
sortField: field,
|
||||||
|
sortOrder: order,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
correspondents: state.correspondents,
|
||||||
|
documentTypes: state.documentTypes,
|
||||||
|
storagePaths: state.storagePaths,
|
||||||
|
tags: state.tags,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import 'dart:developer';
|
|||||||
|
|
||||||
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:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:go_router/go_router.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/widgets/dialog_utils/dialog_cancel_button.dart';
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.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/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class EditLabelPage<T extends Label> extends StatelessWidget {
|
class EditLabelPage<T extends Label> extends StatelessWidget {
|
||||||
@@ -55,8 +57,9 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
|||||||
final Future<T> Function(BuildContext context, T label) onSubmit;
|
final Future<T> Function(BuildContext context, T label) onSubmit;
|
||||||
final Future<void> Function(BuildContext context, T label) onDelete;
|
final Future<void> Function(BuildContext context, T label) onDelete;
|
||||||
final bool canDelete;
|
final bool canDelete;
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
|
||||||
const EditLabelForm({
|
EditLabelForm({
|
||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.fromJsonT,
|
required this.fromJsonT,
|
||||||
@@ -68,26 +71,32 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return PopWithUnsavedChanges(
|
||||||
appBar: AppBar(
|
hasChangesPredicate: () {
|
||||||
title: Text(S.of(context)!.edit),
|
return _formKey.currentState?.isDirty ?? false;
|
||||||
actions: [
|
},
|
||||||
IconButton(
|
child: Scaffold(
|
||||||
onPressed: canDelete ? () => _onDelete(context) : null,
|
appBar: AppBar(
|
||||||
icon: const Icon(Icons.delete),
|
title: Text(S.of(context)!.edit),
|
||||||
),
|
actions: [
|
||||||
],
|
IconButton(
|
||||||
),
|
onPressed: canDelete ? () => _onDelete(context) : null,
|
||||||
body: LabelForm<T>(
|
icon: const Icon(Icons.delete),
|
||||||
autofocusNameField: false,
|
),
|
||||||
initialValue: label,
|
],
|
||||||
fromJsonT: fromJsonT,
|
),
|
||||||
submitButtonConfig: SubmitButtonConfig<T>(
|
body: LabelForm<T>(
|
||||||
icon: const Icon(Icons.save),
|
formKey: _formKey,
|
||||||
label: Text(S.of(context)!.saveChanges),
|
autofocusNameField: false,
|
||||||
onSubmit: (label) => onSubmit(context, label),
|
initialValue: label,
|
||||||
|
fromJsonT: fromJsonT,
|
||||||
|
submitButtonConfig: SubmitButtonConfig<T>(
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
label: Text(S.of(context)!.saveChanges),
|
||||||
|
onSubmit: (label) => onSubmit(context, label),
|
||||||
|
),
|
||||||
|
additionalFields: additionalFields,
|
||||||
),
|
),
|
||||||
additionalFields: additionalFields,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -119,11 +128,11 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
|||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
log("An error occurred!", error: error, stackTrace: stackTrace);
|
log("An error occurred!", error: error, stackTrace: stackTrace);
|
||||||
}
|
}
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onDelete(context, label);
|
onDelete(context, label);
|
||||||
Navigator.pop(context);
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import 'package:paperless_mobile/features/labels/storage_path/view/widgets/stora
|
|||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class AddStoragePathPage extends StatelessWidget {
|
class AddStoragePathPage extends StatelessWidget {
|
||||||
final String? initalName;
|
final String? initialName;
|
||||||
const AddStoragePathPage({Key? key, this.initalName}) : super(key: key);
|
const AddStoragePathPage({Key? key, this.initialName}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -19,7 +19,7 @@ class AddStoragePathPage extends StatelessWidget {
|
|||||||
child: AddLabelPage<StoragePath>(
|
child: AddLabelPage<StoragePath>(
|
||||||
pageTitle: Text(S.of(context)!.addStoragePath),
|
pageTitle: Text(S.of(context)!.addStoragePath),
|
||||||
fromJsonT: StoragePath.fromJson,
|
fromJsonT: StoragePath.fromJson,
|
||||||
initialName: initalName,
|
initialName: initialName,
|
||||||
onSubmit: (context, label) =>
|
onSubmit: (context, label) =>
|
||||||
context.read<EditLabelCubit>().addStoragePath(label),
|
context.read<EditLabelCubit>().addStoragePath(label),
|
||||||
additionalFields: const [
|
additionalFields: const [
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
|
|||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class AddTagPage extends StatelessWidget {
|
class AddTagPage extends StatelessWidget {
|
||||||
final String? initialValue;
|
final String? initialName;
|
||||||
const AddTagPage({Key? key, this.initialValue}) : super(key: key);
|
const AddTagPage({Key? key, this.initialName}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -22,7 +22,7 @@ class AddTagPage extends StatelessWidget {
|
|||||||
child: AddLabelPage<Tag>(
|
child: AddLabelPage<Tag>(
|
||||||
pageTitle: Text(S.of(context)!.addTag),
|
pageTitle: Text(S.of(context)!.addTag),
|
||||||
fromJsonT: Tag.fromJson,
|
fromJsonT: Tag.fromJson,
|
||||||
initialName: initialValue,
|
initialName: initialName,
|
||||||
onSubmit: (context, label) =>
|
onSubmit: (context, label) =>
|
||||||
context.read<EditLabelCubit>().addTag(label),
|
context.read<EditLabelCubit>().addTag(label),
|
||||||
additionalFields: [
|
additionalFields: [
|
||||||
@@ -37,9 +37,16 @@ class AddTagPage extends StatelessWidget {
|
|||||||
.withOpacity(1.0),
|
.withOpacity(1.0),
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
FormBuilderField<bool>(
|
||||||
name: Tag.isInboxTagKey,
|
name: Tag.isInboxTagKey,
|
||||||
title: Text(S.of(context)!.inboxTag),
|
initialValue: false,
|
||||||
|
builder: (field) {
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: field.value,
|
||||||
|
title: Text(S.of(context)!.inboxTag),
|
||||||
|
onChanged: (value) => field.didChange(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ class EditCorrespondentPage extends StatelessWidget {
|
|||||||
context.read<EditLabelCubit>().replaceCorrespondent(label),
|
context.read<EditLabelCubit>().replaceCorrespondent(label),
|
||||||
onDelete: (context, label) =>
|
onDelete: (context, label) =>
|
||||||
context.read<EditLabelCubit>().removeCorrespondent(label),
|
context.read<EditLabelCubit>().removeCorrespondent(label),
|
||||||
canDelete:
|
canDelete: context
|
||||||
LocalUserAccount.current.paperlessUser.canDeleteCorrespondents,
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canDeleteCorrespondents,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ class EditDocumentTypePage extends StatelessWidget {
|
|||||||
context.read<EditLabelCubit>().replaceDocumentType(label),
|
context.read<EditLabelCubit>().replaceDocumentType(label),
|
||||||
onDelete: (context, label) =>
|
onDelete: (context, label) =>
|
||||||
context.read<EditLabelCubit>().removeDocumentType(label),
|
context.read<EditLabelCubit>().removeDocumentType(label),
|
||||||
canDelete:
|
canDelete: context
|
||||||
LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes,
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canDeleteDocumentTypes,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ class EditStoragePathPage extends StatelessWidget {
|
|||||||
context.read<EditLabelCubit>().replaceStoragePath(label),
|
context.read<EditLabelCubit>().replaceStoragePath(label),
|
||||||
onDelete: (context, label) =>
|
onDelete: (context, label) =>
|
||||||
context.read<EditLabelCubit>().removeStoragePath(label),
|
context.read<EditLabelCubit>().removeStoragePath(label),
|
||||||
canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths,
|
canDelete: context
|
||||||
|
.watch<LocalUserAccount>()
|
||||||
|
.paperlessUser
|
||||||
|
.canDeleteStoragePaths,
|
||||||
additionalFields: [
|
additionalFields: [
|
||||||
StoragePathAutofillFormBuilderField(
|
StoragePathAutofillFormBuilderField(
|
||||||
name: StoragePath.pathKey,
|
name: StoragePath.pathKey,
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class EditTagPage extends StatelessWidget {
|
|||||||
context.read<EditLabelCubit>().replaceTag(label),
|
context.read<EditLabelCubit>().replaceTag(label),
|
||||||
onDelete: (context, label) =>
|
onDelete: (context, label) =>
|
||||||
context.read<EditLabelCubit>().removeTag(label),
|
context.read<EditLabelCubit>().removeTag(label),
|
||||||
canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags,
|
canDelete:
|
||||||
|
context.watch<LocalUserAccount>().paperlessUser.canDeleteTags,
|
||||||
additionalFields: [
|
additionalFields: [
|
||||||
FormBuilderColorPickerField(
|
FormBuilderColorPickerField(
|
||||||
initialValue: tag.color,
|
initialValue: tag.color,
|
||||||
@@ -37,10 +38,16 @@ class EditTagPage extends StatelessWidget {
|
|||||||
colorPickerType: ColorPickerType.materialPicker,
|
colorPickerType: ColorPickerType.materialPicker,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
FormBuilderField<bool>(
|
||||||
initialValue: tag.isInboxTag,
|
|
||||||
name: Tag.isInboxTagKey,
|
name: Tag.isInboxTagKey,
|
||||||
title: Text(S.of(context)!.inboxTag),
|
initialValue: tag.isInboxTag,
|
||||||
|
builder: (field) {
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: field.value,
|
||||||
|
title: Text(S.of(context)!.inboxTag),
|
||||||
|
onChanged: (value) => field.didChange(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
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:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
|
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class SubmitButtonConfig<T extends Label> {
|
class SubmitButtonConfig<T extends Label> {
|
||||||
@@ -34,6 +33,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
|
|||||||
final List<Widget> additionalFields;
|
final List<Widget> additionalFields;
|
||||||
|
|
||||||
final bool autofocusNameField;
|
final bool autofocusNameField;
|
||||||
|
final GlobalKey<FormBuilderState>? formKey;
|
||||||
|
|
||||||
const LabelForm({
|
const LabelForm({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -42,6 +42,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
|
|||||||
this.additionalFields = const [],
|
this.additionalFields = const [],
|
||||||
required this.submitButtonConfig,
|
required this.submitButtonConfig,
|
||||||
required this.autofocusNameField,
|
required this.autofocusNameField,
|
||||||
|
this.formKey,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,7 +50,7 @@ class LabelForm<T extends Label> extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
late final GlobalKey<FormBuilderState> _formKey;
|
||||||
|
|
||||||
late bool _enableMatchFormField;
|
late bool _enableMatchFormField;
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_formKey = widget.formKey ?? GlobalKey<FormBuilderState>();
|
||||||
var matchingAlgorithm = (widget.initialValue?.matchingAlgorithm ??
|
var matchingAlgorithm = (widget.initialValue?.matchingAlgorithm ??
|
||||||
MatchingAlgorithm.defaultValue);
|
MatchingAlgorithm.defaultValue);
|
||||||
_enableMatchFormField = matchingAlgorithm != MatchingAlgorithm.auto &&
|
_enableMatchFormField = matchingAlgorithm != MatchingAlgorithm.auto &&
|
||||||
@@ -68,11 +70,12 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<MatchingAlgorithm> selectableMatchingAlgorithmValues =
|
List<MatchingAlgorithm> selectableMatchingAlgorithmValues =
|
||||||
getSelectableMatchingAlgorithmValues(
|
getSelectableMatchingAlgorithmValues(
|
||||||
context.watch<ApiVersion>().hasMultiUserSupport,
|
context.watch<LocalUserAccount>().hasMultiUserSupport,
|
||||||
);
|
);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
heroTag: "fab_label_form",
|
||||||
icon: widget.submitButtonConfig.icon,
|
icon: widget.submitButtonConfig.icon,
|
||||||
label: widget.submitButtonConfig.label,
|
label: widget.submitButtonConfig.label,
|
||||||
onPressed: _onSubmit,
|
onPressed: _onSubmit,
|
||||||
@@ -134,10 +137,16 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
|||||||
initialValue: widget.initialValue?.match,
|
initialValue: widget.initialValue?.match,
|
||||||
onChanged: (val) => setState(() => _errors = {}),
|
onChanged: (val) => setState(() => _errors = {}),
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
FormBuilderField<bool>(
|
||||||
name: Label.isInsensitiveKey,
|
name: Label.isInsensitiveKey,
|
||||||
initialValue: widget.initialValue?.isInsensitive ?? true,
|
initialValue: widget.initialValue?.isInsensitive ?? true,
|
||||||
title: Text(S.of(context)!.caseIrrelevant),
|
builder: (field) {
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: field.value,
|
||||||
|
title: Text(S.of(context)!.caseIrrelevant),
|
||||||
|
onChanged: (value) => field.didChange(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
...widget.additionalFields,
|
...widget.additionalFields,
|
||||||
].padded(),
|
].padded(),
|
||||||
@@ -167,7 +176,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
|||||||
};
|
};
|
||||||
final parsed = widget.fromJsonT(mergedJson);
|
final parsed = widget.fromJsonT(mergedJson);
|
||||||
final createdLabel = await widget.submitButtonConfig.onSubmit(parsed);
|
final createdLabel = await widget.submitButtonConfig.onSubmit(parsed);
|
||||||
Navigator.pop(context, createdLabel);
|
context.pop(createdLabel);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} on PaperlessFormValidationException catch (exception) {
|
} on PaperlessFormValidationException catch (exception) {
|
||||||
|
|||||||
@@ -1,330 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|
||||||
import 'package:paperless_mobile/core/global/constants.dart';
|
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.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/service/file_description.dart';
|
|
||||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_scan/view/scanner_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/inbox/cubit/inbox_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/inbox/view/pages/inbox_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/sharing/share_intent_queue.dart';
|
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|
||||||
import 'package:responsive_builder/responsive_builder.dart';
|
|
||||||
|
|
||||||
/// Wrapper around all functionality for a logged in user.
|
|
||||||
/// Performs initialization logic.
|
|
||||||
class HomePage extends StatefulWidget {
|
|
||||||
final int paperlessApiVersion;
|
|
||||||
const HomePage({Key? key, required this.paperlessApiVersion})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_HomePageState createState() => _HomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|
||||||
int _currentIndex = 0;
|
|
||||||
Timer? _inboxTimer;
|
|
||||||
late final StreamSubscription _shareMediaSubscription;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
|
|
||||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
|
||||||
.getValue()!
|
|
||||||
.currentLoggedInUser!;
|
|
||||||
// For sharing files coming from outside the app while the app is still opened
|
|
||||||
_shareMediaSubscription = ReceiveSharingIntent.getMediaStream().listen(
|
|
||||||
(files) =>
|
|
||||||
ShareIntentQueue.instance.addAll(files, userId: currentUser));
|
|
||||||
// For sharing files coming from outside the app while the app is closed
|
|
||||||
ReceiveSharingIntent.getInitialMedia().then((files) =>
|
|
||||||
ShareIntentQueue.instance.addAll(files, userId: currentUser));
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
|
||||||
_listenForReceivedFiles();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenToInboxChanges() {
|
|
||||||
if (LocalUserAccount.current.paperlessUser.canViewTags) {
|
|
||||||
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
|
|
||||||
if (!mounted) {
|
|
||||||
timer.cancel();
|
|
||||||
} else {
|
|
||||||
context.read<InboxCubit>().refreshItemsInInboxCount();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
switch (state) {
|
|
||||||
case AppLifecycleState.resumed:
|
|
||||||
log('App is now in foreground');
|
|
||||||
context.read<ConnectivityCubit>().reload();
|
|
||||||
log("Reloaded device connectivity state");
|
|
||||||
if (!(_inboxTimer?.isActive ?? true)) {
|
|
||||||
_listenToInboxChanges();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case AppLifecycleState.inactive:
|
|
||||||
case AppLifecycleState.paused:
|
|
||||||
case AppLifecycleState.detached:
|
|
||||||
default:
|
|
||||||
log('App is now in background');
|
|
||||||
_inboxTimer?.cancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
_inboxTimer?.cancel();
|
|
||||||
_shareMediaSubscription.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenForReceivedFiles() async {
|
|
||||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
|
||||||
.getValue()!
|
|
||||||
.currentLoggedInUser!;
|
|
||||||
if (ShareIntentQueue.instance.userHasUnhandlesFiles(currentUser)) {
|
|
||||||
await _handleReceivedFile(ShareIntentQueue.instance.pop(currentUser)!);
|
|
||||||
}
|
|
||||||
ShareIntentQueue.instance.addListener(() async {
|
|
||||||
final queue = ShareIntentQueue.instance;
|
|
||||||
while (queue.userHasUnhandlesFiles(currentUser)) {
|
|
||||||
final file = queue.pop(currentUser)!;
|
|
||||||
await _handleReceivedFile(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isFileTypeSupported(SharedMediaFile file) {
|
|
||||||
return supportedFileExtensions.contains(
|
|
||||||
file.path.split('.').last.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleReceivedFile(final SharedMediaFile file) async {
|
|
||||||
SharedMediaFile mediaFile;
|
|
||||||
if (Platform.isIOS) {
|
|
||||||
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
|
|
||||||
mediaFile = SharedMediaFile(
|
|
||||||
file.path.replaceAll('file://', ''),
|
|
||||||
file.thumbnail,
|
|
||||||
file.duration,
|
|
||||||
file.type,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
mediaFile = file;
|
|
||||||
}
|
|
||||||
debugPrint("Consuming media file: ${mediaFile.path}");
|
|
||||||
if (!_isFileTypeSupported(mediaFile)) {
|
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg: translateError(context, ErrorCode.unsupportedFileFormat),
|
|
||||||
);
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
// As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines.
|
|
||||||
await SystemNavigator.pop();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!LocalUserAccount.current.paperlessUser.canCreateDocuments) {
|
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg: "You do not have the permissions to upload documents.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final fileDescription = FileDescription.fromPath(mediaFile.path);
|
|
||||||
if (await File(mediaFile.path).exists()) {
|
|
||||||
final bytes = await File(mediaFile.path).readAsBytes();
|
|
||||||
final result = await pushDocumentUploadPreparationPage(
|
|
||||||
context,
|
|
||||||
bytes: bytes,
|
|
||||||
filename: fileDescription.filename,
|
|
||||||
title: fileDescription.filename,
|
|
||||||
fileExtension: fileDescription.extension,
|
|
||||||
);
|
|
||||||
if (result?.success ?? false) {
|
|
||||||
await Fluttertoast.showToast(
|
|
||||||
msg: S.of(context)!.documentSuccessfullyUploadedProcessing,
|
|
||||||
);
|
|
||||||
SystemNavigator.pop();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg: S.of(context)!.couldNotAccessReceivedFile,
|
|
||||||
toastLength: Toast.LENGTH_LONG,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final destinations = [
|
|
||||||
RouteDescription(
|
|
||||||
icon: const Icon(Icons.description_outlined),
|
|
||||||
selectedIcon: Icon(
|
|
||||||
Icons.description,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
label: S.of(context)!.documents,
|
|
||||||
),
|
|
||||||
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
|
|
||||||
RouteDescription(
|
|
||||||
icon: const Icon(Icons.document_scanner_outlined),
|
|
||||||
selectedIcon: Icon(
|
|
||||||
Icons.document_scanner,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
label: S.of(context)!.scanner,
|
|
||||||
),
|
|
||||||
RouteDescription(
|
|
||||||
icon: const Icon(Icons.sell_outlined),
|
|
||||||
selectedIcon: Icon(
|
|
||||||
Icons.sell,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
label: S.of(context)!.labels,
|
|
||||||
),
|
|
||||||
if (LocalUserAccount.current.paperlessUser.canViewTags)
|
|
||||||
RouteDescription(
|
|
||||||
icon: const Icon(Icons.inbox_outlined),
|
|
||||||
selectedIcon: Icon(
|
|
||||||
Icons.inbox,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
label: S.of(context)!.inbox,
|
|
||||||
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Badge.count(
|
|
||||||
isLabelVisible: state.itemsInInboxCount > 0,
|
|
||||||
count: state.itemsInInboxCount,
|
|
||||||
child: icon,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
final routes = <Widget>[
|
|
||||||
const DocumentsPage(),
|
|
||||||
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
|
|
||||||
const ScannerPage(),
|
|
||||||
const LabelsPage(),
|
|
||||||
if (LocalUserAccount.current.paperlessUser.canViewTags) const InboxPage(),
|
|
||||||
];
|
|
||||||
return MultiBlocListener(
|
|
||||||
listeners: [
|
|
||||||
BlocListener<ConnectivityCubit, ConnectivityState>(
|
|
||||||
// If app was started offline, load data once it comes back online.
|
|
||||||
listenWhen: (previous, current) =>
|
|
||||||
previous != ConnectivityState.connected &&
|
|
||||||
current == ConnectivityState.connected,
|
|
||||||
listener: (context, state) async {
|
|
||||||
try {
|
|
||||||
debugPrint(
|
|
||||||
"[HomePage] BlocListener#listener: "
|
|
||||||
"Loading saved views and labels...",
|
|
||||||
);
|
|
||||||
await Future.wait([
|
|
||||||
context.read<LabelRepository>().initialize(),
|
|
||||||
context.read<SavedViewRepository>().initialize(),
|
|
||||||
]);
|
|
||||||
debugPrint("[HomePage] BlocListener#listener: "
|
|
||||||
"Saved views and labels successfully loaded.");
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
debugPrint(
|
|
||||||
'[HomePage] BlocListener.listener: '
|
|
||||||
'An error occurred while loading saved views and labels.\n'
|
|
||||||
'${error.toString()}',
|
|
||||||
);
|
|
||||||
debugPrintStack(stackTrace: stackTrace);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
BlocListener<TaskStatusCubit, TaskStatusState>(
|
|
||||||
listener: (context, state) {
|
|
||||||
if (state.task != null) {
|
|
||||||
// Handle local notifications on task change (only when app is running for now).
|
|
||||||
context
|
|
||||||
.read<LocalNotificationService>()
|
|
||||||
.notifyTaskChanged(state.task!);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: ResponsiveBuilder(
|
|
||||||
builder: (context, sizingInformation) {
|
|
||||||
if (!sizingInformation.isMobile) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Row(
|
|
||||||
children: [
|
|
||||||
NavigationRail(
|
|
||||||
labelType: NavigationRailLabelType.all,
|
|
||||||
destinations: destinations
|
|
||||||
.map((e) => e.toNavigationRailDestination())
|
|
||||||
.toList(),
|
|
||||||
selectedIndex: _currentIndex,
|
|
||||||
onDestinationSelected: _onNavigationChanged,
|
|
||||||
),
|
|
||||||
const VerticalDivider(thickness: 1, width: 1),
|
|
||||||
Expanded(
|
|
||||||
child: routes[_currentIndex],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Scaffold(
|
|
||||||
bottomNavigationBar: NavigationBar(
|
|
||||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
|
||||||
elevation: 4.0,
|
|
||||||
selectedIndex: _currentIndex,
|
|
||||||
onDestinationSelected: _onNavigationChanged,
|
|
||||||
destinations:
|
|
||||||
destinations.map((e) => e.toNavigationDestination()).toList(),
|
|
||||||
),
|
|
||||||
body: routes[_currentIndex],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onNavigationChanged(index) {
|
|
||||||
if (_currentIndex != index) {
|
|
||||||
setState(() => _currentIndex = index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:hive_flutter/adapters.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.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/saved_view_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/user_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/dio_file_service.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/home/view/home_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class HomeRoute extends StatelessWidget {
|
|
||||||
/// The id of the currently authenticated user (e.g. demo@paperless.example.com)
|
|
||||||
final String localUserId;
|
|
||||||
|
|
||||||
/// The Paperless API version of the currently connected instance
|
|
||||||
final int paperlessApiVersion;
|
|
||||||
|
|
||||||
// A factory providing the API implementations given an API version
|
|
||||||
final PaperlessApiFactory paperlessProviderFactory;
|
|
||||||
|
|
||||||
const HomeRoute({
|
|
||||||
super.key,
|
|
||||||
required this.paperlessApiVersion,
|
|
||||||
required this.paperlessProviderFactory,
|
|
||||||
required this.localUserId,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GlobalSettingsBuilder(
|
|
||||||
builder: (context, settings) {
|
|
||||||
final currentLocalUserId = settings.currentLoggedInUser;
|
|
||||||
if (currentLocalUserId == null) {
|
|
||||||
// This is the case when the current user logs out of the app.
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
final currentUser =
|
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
|
||||||
.get(currentLocalUserId)!;
|
|
||||||
final apiVersion = ApiVersion(paperlessApiVersion);
|
|
||||||
return MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: apiVersion),
|
|
||||||
Provider<CacheManager>(
|
|
||||||
create: (context) => CacheManager(
|
|
||||||
Config(
|
|
||||||
// Isolated cache per user.
|
|
||||||
localUserId,
|
|
||||||
fileService:
|
|
||||||
DioFileService(context.read<SessionManager>().client),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ProxyProvider<SessionManager, PaperlessDocumentsApi>(
|
|
||||||
update: (context, value, previous) =>
|
|
||||||
paperlessProviderFactory.createDocumentsApi(
|
|
||||||
value.client,
|
|
||||||
apiVersion: paperlessApiVersion,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ProxyProvider<SessionManager, PaperlessLabelsApi>(
|
|
||||||
update: (context, value, previous) =>
|
|
||||||
paperlessProviderFactory.createLabelsApi(
|
|
||||||
value.client,
|
|
||||||
apiVersion: paperlessApiVersion,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ProxyProvider<SessionManager, PaperlessSavedViewsApi>(
|
|
||||||
update: (context, value, previous) =>
|
|
||||||
paperlessProviderFactory.createSavedViewsApi(
|
|
||||||
value.client,
|
|
||||||
apiVersion: paperlessApiVersion,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ProxyProvider<SessionManager, PaperlessServerStatsApi>(
|
|
||||||
update: (context, value, previous) =>
|
|
||||||
paperlessProviderFactory.createServerStatsApi(
|
|
||||||
value.client,
|
|
||||||
apiVersion: paperlessApiVersion,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ProxyProvider<SessionManager, PaperlessTasksApi>(
|
|
||||||
update: (context, value, previous) =>
|
|
||||||
paperlessProviderFactory.createTasksApi(
|
|
||||||
value.client,
|
|
||||||
apiVersion: paperlessApiVersion,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (apiVersion.hasMultiUserSupport)
|
|
||||||
ProxyProvider<SessionManager, PaperlessUserApiV3>(
|
|
||||||
update: (context, value, previous) => PaperlessUserApiV3Impl(
|
|
||||||
value.client,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
builder: (context, child) {
|
|
||||||
return MultiProvider(
|
|
||||||
providers: [
|
|
||||||
ProxyProvider<PaperlessLabelsApi, LabelRepository>(
|
|
||||||
update: (context, value, previous) {
|
|
||||||
final repo = LabelRepository(value);
|
|
||||||
if (currentUser.paperlessUser.canViewCorrespondents) {
|
|
||||||
repo.findAllCorrespondents();
|
|
||||||
}
|
|
||||||
if (currentUser.paperlessUser.canViewDocumentTypes) {
|
|
||||||
repo.findAllDocumentTypes();
|
|
||||||
}
|
|
||||||
if (currentUser.paperlessUser.canViewTags) {
|
|
||||||
repo.findAllTags();
|
|
||||||
}
|
|
||||||
if (currentUser.paperlessUser.canViewStoragePaths) {
|
|
||||||
repo.findAllStoragePaths();
|
|
||||||
}
|
|
||||||
return repo;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
|
|
||||||
update: (context, value, previous) {
|
|
||||||
final repo = SavedViewRepository(value);
|
|
||||||
if (currentUser.paperlessUser.canViewSavedViews) {
|
|
||||||
repo.initialize();
|
|
||||||
}
|
|
||||||
return repo;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
builder: (context, child) {
|
|
||||||
return MultiProvider(
|
|
||||||
providers: [
|
|
||||||
ProxyProvider3<
|
|
||||||
PaperlessDocumentsApi,
|
|
||||||
DocumentChangedNotifier,
|
|
||||||
LabelRepository,
|
|
||||||
DocumentsCubit>(
|
|
||||||
update:
|
|
||||||
(context, docApi, notifier, labelRepo, previous) =>
|
|
||||||
DocumentsCubit(
|
|
||||||
docApi,
|
|
||||||
notifier,
|
|
||||||
labelRepo,
|
|
||||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
|
||||||
.get(currentLocalUserId)!,
|
|
||||||
)..initialize(),
|
|
||||||
),
|
|
||||||
Provider(
|
|
||||||
create: (context) =>
|
|
||||||
DocumentScannerCubit(context.read())),
|
|
||||||
ProxyProvider4<
|
|
||||||
PaperlessDocumentsApi,
|
|
||||||
PaperlessServerStatsApi,
|
|
||||||
LabelRepository,
|
|
||||||
DocumentChangedNotifier,
|
|
||||||
InboxCubit>(
|
|
||||||
update: (context, docApi, statsApi, labelRepo, notifier,
|
|
||||||
previous) =>
|
|
||||||
InboxCubit(
|
|
||||||
docApi,
|
|
||||||
statsApi,
|
|
||||||
labelRepo,
|
|
||||||
notifier,
|
|
||||||
)..initialize(),
|
|
||||||
),
|
|
||||||
ProxyProvider<SavedViewRepository, SavedViewCubit>(
|
|
||||||
update: (context, savedViewRepo, previous) =>
|
|
||||||
SavedViewCubit(savedViewRepo),
|
|
||||||
),
|
|
||||||
ProxyProvider<LabelRepository, LabelCubit>(
|
|
||||||
update: (context, value, previous) => LabelCubit(value),
|
|
||||||
),
|
|
||||||
ProxyProvider<PaperlessTasksApi, TaskStatusCubit>(
|
|
||||||
update: (context, value, previous) =>
|
|
||||||
TaskStatusCubit(value),
|
|
||||||
),
|
|
||||||
if (paperlessApiVersion >= 3)
|
|
||||||
ProxyProvider<PaperlessUserApiV3, UserRepository>(
|
|
||||||
update: (context, value, previous) =>
|
|
||||||
UserRepository(value)..initialize(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: HomePage(paperlessApiVersion: paperlessApiVersion),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
|
import 'package:paperless_mobile/core/factory/paperless_api_factory.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/user_repository.dart';
|
||||||
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/dio_file_service.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||||
|
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class HomeShellWidget extends StatelessWidget {
|
||||||
|
/// The id of the currently authenticated user (e.g. demo@paperless.example.com)
|
||||||
|
final String localUserId;
|
||||||
|
|
||||||
|
/// The Paperless API version of the currently connected instance
|
||||||
|
final int paperlessApiVersion;
|
||||||
|
|
||||||
|
// A factory providing the API implementations given an API version
|
||||||
|
final PaperlessApiFactory paperlessProviderFactory;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const HomeShellWidget({
|
||||||
|
super.key,
|
||||||
|
required this.paperlessApiVersion,
|
||||||
|
required this.paperlessProviderFactory,
|
||||||
|
required this.localUserId,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GlobalSettingsBuilder(
|
||||||
|
builder: (context, settings) {
|
||||||
|
final currentUserId = settings.loggedInUserId;
|
||||||
|
final apiVersion = ApiVersion(paperlessApiVersion);
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable:
|
||||||
|
Hive.localUserAccountBox.listenable(keys: [currentUserId]),
|
||||||
|
builder: (context, box, _) {
|
||||||
|
if (currentUserId == null) {
|
||||||
|
//This only happens during logout...
|
||||||
|
//TODO: Find way so this does not occur anymore
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final currentLocalUser = box.get(currentUserId)!;
|
||||||
|
return MultiProvider(
|
||||||
|
key: ValueKey(currentUserId),
|
||||||
|
providers: [
|
||||||
|
Provider.value(value: currentLocalUser),
|
||||||
|
Provider.value(value: apiVersion),
|
||||||
|
Provider(
|
||||||
|
create: (context) => CacheManager(
|
||||||
|
Config(
|
||||||
|
// Isolated cache per user.
|
||||||
|
localUserId,
|
||||||
|
fileService:
|
||||||
|
DioFileService(context.read<SessionManager>().client),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) =>
|
||||||
|
paperlessProviderFactory.createDocumentsApi(
|
||||||
|
context.read<SessionManager>().client,
|
||||||
|
apiVersion: paperlessApiVersion,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) => paperlessProviderFactory.createLabelsApi(
|
||||||
|
context.read<SessionManager>().client,
|
||||||
|
apiVersion: paperlessApiVersion,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) =>
|
||||||
|
paperlessProviderFactory.createSavedViewsApi(
|
||||||
|
context.read<SessionManager>().client,
|
||||||
|
apiVersion: paperlessApiVersion,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) =>
|
||||||
|
paperlessProviderFactory.createServerStatsApi(
|
||||||
|
context.read<SessionManager>().client,
|
||||||
|
apiVersion: paperlessApiVersion,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) => paperlessProviderFactory.createTasksApi(
|
||||||
|
context.read<SessionManager>().client,
|
||||||
|
apiVersion: paperlessApiVersion,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (currentLocalUser.hasMultiUserSupport)
|
||||||
|
Provider(
|
||||||
|
create: (context) => PaperlessUserApiV3Impl(
|
||||||
|
context.read<SessionManager>().client,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
builder: (context, _) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider(
|
||||||
|
create: (context) {
|
||||||
|
final repo = LabelRepository(context.read());
|
||||||
|
if (currentLocalUser
|
||||||
|
.paperlessUser.canViewCorrespondents) {
|
||||||
|
repo.findAllCorrespondents();
|
||||||
|
}
|
||||||
|
if (currentLocalUser
|
||||||
|
.paperlessUser.canViewDocumentTypes) {
|
||||||
|
repo.findAllDocumentTypes();
|
||||||
|
}
|
||||||
|
if (currentLocalUser.paperlessUser.canViewTags) {
|
||||||
|
repo.findAllTags();
|
||||||
|
}
|
||||||
|
if (currentLocalUser
|
||||||
|
.paperlessUser.canViewStoragePaths) {
|
||||||
|
repo.findAllStoragePaths();
|
||||||
|
}
|
||||||
|
return repo;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) {
|
||||||
|
final repo = SavedViewRepository(context.read());
|
||||||
|
if (currentLocalUser.paperlessUser.canViewSavedViews) {
|
||||||
|
repo.initialize();
|
||||||
|
}
|
||||||
|
return repo;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
builder: (context, _) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider(
|
||||||
|
lazy: false,
|
||||||
|
create: (context) => DocumentsCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
Hive.box<LocalUserAppState>(
|
||||||
|
HiveBoxes.localUserAppState)
|
||||||
|
.get(currentUserId)!,
|
||||||
|
context.read(),
|
||||||
|
)..initialize(),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) =>
|
||||||
|
DocumentScannerCubit(context.read())
|
||||||
|
..initialize(),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) {
|
||||||
|
final inboxCubit = InboxCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
);
|
||||||
|
if (currentLocalUser.paperlessUser.canViewInbox) {
|
||||||
|
inboxCubit.initialize();
|
||||||
|
}
|
||||||
|
return inboxCubit;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) => SavedViewCubit(
|
||||||
|
context.read(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Provider(
|
||||||
|
create: (context) => LabelCubit(
|
||||||
|
context.read(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (context) => PendingTasksNotifier(
|
||||||
|
context.read(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (currentLocalUser.hasMultiUserSupport)
|
||||||
|
Provider(
|
||||||
|
create: (context) => UserRepository(
|
||||||
|
context.read(),
|
||||||
|
)..initialize(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
class ApiVersion {
|
class ApiVersion {
|
||||||
final int version;
|
final int version;
|
||||||
|
|
||||||
ApiVersion(this.version);
|
const ApiVersion(this.version);
|
||||||
|
|
||||||
|
|
||||||
bool get hasMultiUserSupport => version >= 3;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class RouteDescription {
|
|
||||||
final String label;
|
|
||||||
final Icon icon;
|
|
||||||
final Icon selectedIcon;
|
|
||||||
final Widget Function(Widget icon)? badgeBuilder;
|
|
||||||
final bool enabled;
|
|
||||||
|
|
||||||
RouteDescription({
|
|
||||||
required this.label,
|
|
||||||
required this.icon,
|
|
||||||
required this.selectedIcon,
|
|
||||||
this.badgeBuilder,
|
|
||||||
this.enabled = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
NavigationDestination toNavigationDestination() {
|
|
||||||
return NavigationDestination(
|
|
||||||
label: label,
|
|
||||||
icon: badgeBuilder?.call(icon) ?? icon,
|
|
||||||
selectedIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationRailDestination toNavigationRailDestination() {
|
|
||||||
return NavigationRailDestination(
|
|
||||||
label: Text(label),
|
|
||||||
icon: icon,
|
|
||||||
selectedIcon: selectedIcon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
BottomNavigationBarItem toBottomNavigationBarItem() {
|
|
||||||
return BottomNavigationBarItem(
|
|
||||||
label: label,
|
|
||||||
icon: badgeBuilder?.call(icon) ?? icon,
|
|
||||||
activeIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
|
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/theme.dart';
|
||||||
|
|
||||||
|
class ScaffoldWithNavigationBar extends StatefulWidget {
|
||||||
|
final UserModel authenticatedUser;
|
||||||
|
final StatefulNavigationShell navigationShell;
|
||||||
|
const ScaffoldWithNavigationBar({
|
||||||
|
super.key,
|
||||||
|
required this.authenticatedUser,
|
||||||
|
required this.navigationShell,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ScaffoldWithNavigationBar> createState() =>
|
||||||
|
ScaffoldWithNavigationBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScaffoldWithNavigationBarState extends State<ScaffoldWithNavigationBar> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: buildOverlayStyle(theme),
|
||||||
|
child: Scaffold(
|
||||||
|
drawer: const AppDrawer(),
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
elevation: 3,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
selectedIndex: widget.navigationShell.currentIndex,
|
||||||
|
onDestinationSelected: (index) {
|
||||||
|
widget.navigationShell.goBranch(
|
||||||
|
index,
|
||||||
|
initialLocation: index == widget.navigationShell.currentIndex,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
destinations: [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(Icons.home_outlined),
|
||||||
|
selectedIcon: Icon(
|
||||||
|
Icons.home,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
label: S.of(context)!.home,
|
||||||
|
),
|
||||||
|
_toggleDestination(
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(Icons.description_outlined),
|
||||||
|
selectedIcon: Icon(
|
||||||
|
Icons.description,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
label: S.of(context)!.documents,
|
||||||
|
),
|
||||||
|
disableWhen: !widget.authenticatedUser.canViewDocuments,
|
||||||
|
),
|
||||||
|
_toggleDestination(
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(Icons.document_scanner_outlined),
|
||||||
|
selectedIcon: Icon(
|
||||||
|
Icons.document_scanner,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
label: S.of(context)!.scanner,
|
||||||
|
),
|
||||||
|
disableWhen: !widget.authenticatedUser.canCreateDocuments,
|
||||||
|
),
|
||||||
|
_toggleDestination(
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(Icons.sell_outlined),
|
||||||
|
selectedIcon: Icon(
|
||||||
|
Icons.sell,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
label: S.of(context)!.labels,
|
||||||
|
),
|
||||||
|
disableWhen: !widget.authenticatedUser.canViewAnyLabel,
|
||||||
|
),
|
||||||
|
_toggleDestination(
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return BlocBuilder<InboxCubit, InboxState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Badge.count(
|
||||||
|
isLabelVisible: state.itemsInInboxCount > 0,
|
||||||
|
count: state.itemsInInboxCount,
|
||||||
|
child: const Icon(Icons.inbox_outlined),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
selectedIcon: BlocBuilder<InboxCubit, InboxState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Badge.count(
|
||||||
|
isLabelVisible: state.itemsInInboxCount > 0 &&
|
||||||
|
widget.authenticatedUser.canViewInbox,
|
||||||
|
count: state.itemsInInboxCount,
|
||||||
|
child: Icon(
|
||||||
|
Icons.inbox,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
label: S.of(context)!.inbox,
|
||||||
|
),
|
||||||
|
disableWhen: !widget.authenticatedUser.canViewInbox,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: widget.navigationShell,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _toggleDestination(
|
||||||
|
Widget destination, {
|
||||||
|
required bool disableWhen,
|
||||||
|
}) {
|
||||||
|
final disabledColor = Theme.of(context).disabledColor;
|
||||||
|
|
||||||
|
final disabledTheme = Theme.of(context).navigationBarTheme.copyWith(
|
||||||
|
labelTextStyle: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall
|
||||||
|
?.copyWith(color: disabledColor),
|
||||||
|
),
|
||||||
|
iconTheme: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).iconTheme.copyWith(color: disabledColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (disableWhen) {
|
||||||
|
return AbsorbPointer(
|
||||||
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(navigationBarTheme: disabledTheme),
|
||||||
|
child: destination,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
|
||||||
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class VerifyIdentityPage extends StatelessWidget {
|
|
||||||
const VerifyIdentityPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
elevation: 0,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
|
||||||
title: Text(S.of(context)!.verifyYourIdentity),
|
|
||||||
),
|
|
||||||
body: UserAccountBuilder(
|
|
||||||
builder: (context, settings) {
|
|
||||||
if (settings == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(S
|
|
||||||
.of(context)!
|
|
||||||
.useTheConfiguredBiometricFactorToAuthenticate)
|
|
||||||
.paddedSymmetrically(horizontal: 16),
|
|
||||||
const Icon(
|
|
||||||
Icons.fingerprint,
|
|
||||||
size: 96,
|
|
||||||
),
|
|
||||||
Wrap(
|
|
||||||
alignment: WrapAlignment.spaceBetween,
|
|
||||||
runAlignment: WrapAlignment.spaceBetween,
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => _logout(context),
|
|
||||||
child: Text(
|
|
||||||
S.of(context)!.disconnect,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => context
|
|
||||||
.read<AuthenticationCubit>()
|
|
||||||
.restoreSessionState(),
|
|
||||||
child: Text(S.of(context)!.verifyIdentity),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(16),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _logout(BuildContext context) {
|
|
||||||
context.read<AuthenticationCubit>().logout();
|
|
||||||
context.read<LabelRepository>().clear();
|
|
||||||
context.read<SavedViewRepository>().clear();
|
|
||||||
HydratedBloc.storage.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||||
|
|
||||||
@@ -19,6 +20,9 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
|
|
||||||
final PaperlessDocumentsApi _documentsApi;
|
final PaperlessDocumentsApi _documentsApi;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
@@ -32,21 +36,35 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
this._statsApi,
|
this._statsApi,
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
) : super(InboxState(
|
this.connectivityStatusService,
|
||||||
labels: _labelRepository.state,
|
) : super(InboxState(labels: _labelRepository.state)) {
|
||||||
)) {
|
|
||||||
notifier.addListener(
|
notifier.addListener(
|
||||||
this,
|
this,
|
||||||
onDeleted: remove,
|
onDeleted: remove,
|
||||||
onUpdated: (document) {
|
onUpdated: (document) {
|
||||||
if (document.tags
|
final hasInboxTag = document.tags
|
||||||
.toSet()
|
.toSet()
|
||||||
.intersection(state.inboxTags.toSet())
|
.intersection(state.inboxTags.toSet())
|
||||||
.isEmpty) {
|
.isNotEmpty;
|
||||||
|
final wasInInboxBeforeUpdate =
|
||||||
|
state.documents.map((e) => e.id).contains(document.id);
|
||||||
|
if (!hasInboxTag && wasInInboxBeforeUpdate) {
|
||||||
|
print(
|
||||||
|
"INBOX: Removing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||||
remove(document);
|
remove(document);
|
||||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
||||||
} else {
|
} else if (hasInboxTag) {
|
||||||
replace(document);
|
if (wasInInboxBeforeUpdate) {
|
||||||
|
print(
|
||||||
|
"INBOX: Replacing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||||
|
replace(document);
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
"INBOX: Adding document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||||
|
_addDocument(document);
|
||||||
|
emit(
|
||||||
|
state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -58,22 +76,20 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
await refreshItemsInInboxCount(false);
|
await refreshItemsInInboxCount(false);
|
||||||
await loadInbox();
|
await loadInbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
|
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
|
||||||
|
debugPrint("Checking for new items in inbox...");
|
||||||
final stats = await _statsApi.getServerStatistics();
|
final stats = await _statsApi.getServerStatistics();
|
||||||
|
|
||||||
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
|
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
|
||||||
await loadInbox();
|
await loadInbox();
|
||||||
}
|
}
|
||||||
emit(
|
emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox));
|
||||||
state.copyWith(
|
|
||||||
itemsInInboxCount: stats.documentsInInbox,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -82,7 +98,6 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
Future<void> loadInbox() async {
|
Future<void> loadInbox() async {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
debugPrint("Initializing inbox...");
|
debugPrint("Initializing inbox...");
|
||||||
|
|
||||||
final inboxTags = await _labelRepository.findAllTags().then(
|
final inboxTags = await _labelRepository.findAllTags().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
@@ -110,11 +125,22 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _addDocument(DocumentModel document) async {
|
||||||
|
emit(state.copyWith(
|
||||||
|
value: [
|
||||||
|
...state.value,
|
||||||
|
PagedSearchResult(
|
||||||
|
count: 1,
|
||||||
|
results: [document],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Fetches inbox tag ids and loads the inbox items (documents).
|
/// Fetches inbox tag ids and loads the inbox items (documents).
|
||||||
///
|
///
|
||||||
Future<void> reloadInbox() async {
|
Future<void> reloadInbox() async {
|
||||||
emit(state.copyWith(hasLoaded: false, isLoading: true));
|
|
||||||
final inboxTags = await _labelRepository.findAllTags().then(
|
final inboxTags = await _labelRepository.findAllTags().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
@@ -131,6 +157,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
}
|
}
|
||||||
emit(state.copyWith(inboxTags: inboxTags));
|
emit(state.copyWith(inboxTags: inboxTags));
|
||||||
updateFilter(
|
updateFilter(
|
||||||
|
emitLoading: false,
|
||||||
filter: DocumentFilter(
|
filter: DocumentFilter(
|
||||||
sortField: SortField.added,
|
sortField: SortField.added,
|
||||||
tags: TagsQuery.ids(include: inboxTags.toList()),
|
tags: TagsQuery.ids(include: inboxTags.toList()),
|
||||||
@@ -151,7 +178,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
document.copyWith(tags: updatedTags),
|
document.copyWith(tags: updatedTags),
|
||||||
);
|
);
|
||||||
// Remove first so document is not replaced first.
|
// Remove first so document is not replaced first.
|
||||||
remove(document);
|
// remove(document);
|
||||||
notifier.notifyUpdated(updatedDocument);
|
notifier.notifyUpdated(updatedDocument);
|
||||||
return tagsToRemove;
|
return tagsToRemove;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
@@ -17,6 +18,7 @@ import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.
|
|||||||
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
|
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
|
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class InboxPage extends StatefulWidget {
|
class InboxPage extends StatefulWidget {
|
||||||
@@ -33,42 +35,99 @@ class _InboxPageState extends State<InboxPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
final pagingScrollController = ScrollController();
|
final pagingScrollController = ScrollController();
|
||||||
|
final _nestedScrollViewKey = GlobalKey<NestedScrollViewState>();
|
||||||
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
|
bool _showExtendedFab = true;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<InboxCubit>().reloadInbox();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_nestedScrollViewKey.currentState!.innerController
|
||||||
|
.addListener(_scrollExtentChangedListener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nestedScrollViewKey.currentState?.innerController
|
||||||
|
.removeListener(_scrollExtentChangedListener);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollExtentChangedListener() {
|
||||||
|
const threshold = 400;
|
||||||
|
final offset =
|
||||||
|
_nestedScrollViewKey.currentState!.innerController.position.pixels;
|
||||||
|
if (offset < threshold && _showExtendedFab == false) {
|
||||||
|
setState(() {
|
||||||
|
_showExtendedFab = true;
|
||||||
|
});
|
||||||
|
} else if (offset >= threshold && _showExtendedFab == true) {
|
||||||
|
setState(() {
|
||||||
|
_showExtendedFab = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final canEditDocument =
|
final canEditDocument =
|
||||||
LocalUserAccount.current.paperlessUser.canEditDocuments;
|
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
|
floatingActionButton: ConnectivityAwareActionWrapper(
|
||||||
builder: (context, state) {
|
offlineBuilder: (context, child) => const SizedBox.shrink(),
|
||||||
if (!state.hasLoaded || state.documents.isEmpty || !canEditDocument) {
|
child: BlocBuilder<InboxCubit, InboxState>(
|
||||||
return const SizedBox.shrink();
|
builder: (context, state) {
|
||||||
}
|
if (!state.hasLoaded ||
|
||||||
return FloatingActionButton.extended(
|
state.documents.isEmpty ||
|
||||||
label: Text(S.of(context)!.allSeen),
|
!canEditDocument) {
|
||||||
icon: const Icon(Icons.done_all),
|
return const SizedBox.shrink();
|
||||||
onPressed: state.hasLoaded && state.documents.isNotEmpty
|
}
|
||||||
? () => _onMarkAllAsSeen(
|
return FloatingActionButton.extended(
|
||||||
state.documents,
|
extendedPadding: _showExtendedFab
|
||||||
state.inboxTags,
|
? null
|
||||||
)
|
: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
: null,
|
heroTag: "inbox_page_fab",
|
||||||
);
|
label: AnimatedSwitcher(
|
||||||
},
|
duration: const Duration(milliseconds: 200),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
axis: Axis.horizontal,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _showExtendedFab
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.done_all),
|
||||||
|
Text(S.of(context)!.allSeen),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const Icon(Icons.done_all),
|
||||||
|
),
|
||||||
|
onPressed: state.hasLoaded && state.documents.isNotEmpty
|
||||||
|
? () => _onMarkAllAsSeen(
|
||||||
|
state.documents,
|
||||||
|
state.inboxTags,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: true,
|
top: true,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
|
key: _nestedScrollViewKey,
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
SliverOverlapAbsorber(
|
SliverSearchBar(titleText: S.of(context)!.inbox),
|
||||||
handle: searchBarHandle,
|
|
||||||
sliver: SliverSearchBar(
|
|
||||||
titleText: S.of(context)!.inbox,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
body: BlocBuilder<InboxCubit, InboxState>(
|
body: BlocBuilder<InboxCubit, InboxState>(
|
||||||
builder: (_, state) {
|
builder: (_, state) {
|
||||||
@@ -213,6 +272,16 @@ class _InboxPageState extends State<InboxPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _onItemDismissed(DocumentModel doc) async {
|
Future<bool> _onItemDismissed(DocumentModel doc) async {
|
||||||
|
if (!context.read<LocalUserAccount>().paperlessUser.canEditDocuments) {
|
||||||
|
showSnackBar(context, S.of(context)!.missingPermissions);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final isConnectedToInternet =
|
||||||
|
await context.read<ConnectivityStatusService>().isConnectedToInternet();
|
||||||
|
if (!isConnectedToInternet) {
|
||||||
|
showSnackBar(context, S.of(context)!.youAreCurrentlyOffline);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final removedTags = await context.read<InboxCubit>().removeFromInbox(doc);
|
final removedTags = await context.read<InboxCubit>().removeFromInbox(doc);
|
||||||
showSnackBar(
|
showSnackBar(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
@@ -15,6 +14,8 @@ import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.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/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
|
||||||
class InboxItemPlaceholder extends StatelessWidget {
|
class InboxItemPlaceholder extends StatelessWidget {
|
||||||
const InboxItemPlaceholder({super.key});
|
const InboxItemPlaceholder({super.key});
|
||||||
@@ -150,11 +151,10 @@ class _InboxItemState extends State<InboxItem> {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
pushDocumentDetailsRoute(
|
DocumentDetailsRoute(
|
||||||
context,
|
$extra: widget.document,
|
||||||
document: widget.document,
|
|
||||||
isLabelClickable: false,
|
isLabelClickable: false,
|
||||||
);
|
).push(context);
|
||||||
},
|
},
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 200,
|
height: 200,
|
||||||
@@ -227,7 +227,9 @@ class _InboxItemState extends State<InboxItem> {
|
|||||||
),
|
),
|
||||||
LimitedBox(
|
LimitedBox(
|
||||||
maxHeight: 56,
|
maxHeight: 56,
|
||||||
child: _buildActions(context),
|
child: ConnectivityAwareActionWrapper(
|
||||||
|
child: _buildActions(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddedOnly(left: 8, top: 8, bottom: 8),
|
).paddedOnly(left: 8, top: 8, bottom: 8),
|
||||||
@@ -238,8 +240,9 @@ class _InboxItemState extends State<InboxItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActions(BuildContext context) {
|
Widget _buildActions(BuildContext context) {
|
||||||
final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments;
|
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||||
final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments;
|
final canEdit = currentUser.canEditDocuments;
|
||||||
|
final canDelete = currentUser.canDeleteDocuments;
|
||||||
final chipShape = RoundedRectangleBorder(
|
final chipShape = RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ 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/features/labels/cubit/label_cubit_mixin.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
|
||||||
|
|
||||||
part 'label_state.dart';
|
|
||||||
part 'label_cubit.freezed.dart';
|
part 'label_cubit.freezed.dart';
|
||||||
|
part 'label_state.dart';
|
||||||
|
|
||||||
class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
|
class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
|
||||||
@override
|
@override
|
||||||
@@ -25,6 +25,15 @@ class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> reload() {
|
||||||
|
return Future.wait([
|
||||||
|
labelRepository.findAllCorrespondents(),
|
||||||
|
labelRepository.findAllDocumentTypes(),
|
||||||
|
labelRepository.findAllTags(),
|
||||||
|
labelRepository.findAllStoragePaths(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
labelRepository.removeListener(this);
|
labelRepository.removeListener(this);
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
|
|
||||||
class StoragePathWidget extends StatelessWidget {
|
|
||||||
final StoragePath? storagePath;
|
|
||||||
final Color? textColor;
|
|
||||||
final bool isClickable;
|
|
||||||
final void Function(int? id)? onSelected;
|
|
||||||
|
|
||||||
const StoragePathWidget({
|
|
||||||
Key? key,
|
|
||||||
this.storagePath,
|
|
||||||
this.textColor,
|
|
||||||
this.isClickable = true,
|
|
||||||
this.onSelected,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AbsorbPointer(
|
|
||||||
absorbing: !isClickable,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => onSelected?.call(storagePath?.id),
|
|
||||||
child: Text(
|
|
||||||
storagePath?.name ?? "-",
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: textColor ?? Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -68,10 +69,12 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showFab = MediaQuery.viewInsetsOf(context).bottom == 0;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
floatingActionButton: widget.allowCreation
|
floatingActionButton: widget.allowCreation && showFab
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
|
heroTag: "fab_tags_form",
|
||||||
onPressed: _onAddTag,
|
onPressed: _onAddTag,
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
)
|
)
|
||||||
@@ -191,7 +194,7 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
final createdTag = await Navigator.of(context).push<Tag?>(
|
final createdTag = await Navigator.of(context).push<Tag?>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddTagPage(
|
builder: (context) => AddTagPage(
|
||||||
initialValue: _textEditingController.text,
|
initialName: _textEditingController.text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -237,10 +240,16 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
var matches = _options
|
var matches = _options
|
||||||
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||||
if (matches.isEmpty && widget.allowCreation) {
|
if (matches.isEmpty && widget.allowCreation) {
|
||||||
yield Text(S.of(context)!.noItemsFound);
|
yield Center(
|
||||||
yield TextButton(
|
child: Column(
|
||||||
child: Text(S.of(context)!.addTag),
|
children: [
|
||||||
onPressed: _onAddTag,
|
Text(S.of(context)!.noItemsFound).padded(),
|
||||||
|
TextButton(
|
||||||
|
child: Text(S.of(context)!.addTag),
|
||||||
|
onPressed: _onAddTag,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (final tag in matches) {
|
for (final tag in matches) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
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_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
@@ -73,7 +74,7 @@ class TagsFormField extends StatelessWidget {
|
|||||||
initialValue: field.value,
|
initialValue: field.value,
|
||||||
allowOnlySelection: allowOnlySelection,
|
allowOnlySelection: allowOnlySelection,
|
||||||
allowCreation: allowCreation &&
|
allowCreation: allowCreation &&
|
||||||
LocalUserAccount.current.paperlessUser.canCreateTags,
|
context.watch<LocalUserAccount>().paperlessUser.canCreateTags,
|
||||||
allowExclude: allowExclude,
|
allowExclude: allowExclude,
|
||||||
),
|
),
|
||||||
onClosed: (data) {
|
onClosed: (data) {
|
||||||
|
|||||||
@@ -7,23 +7,14 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
|||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
|
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
|
||||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/edit_correspondent_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||||
|
|
||||||
class LabelsPage extends StatefulWidget {
|
class LabelsPage extends StatefulWidget {
|
||||||
const LabelsPage({Key? key}) : super(key: key);
|
const LabelsPage({Key? key}) : super(key: key);
|
||||||
@@ -40,6 +31,7 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
SliverOverlapAbsorberHandle();
|
SliverOverlapAbsorberHandle();
|
||||||
|
|
||||||
late final TabController _tabController;
|
late final TabController _tabController;
|
||||||
|
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
||||||
int _calculateTabCount(UserModel user) => [
|
int _calculateTabCount(UserModel user) => [
|
||||||
@@ -52,12 +44,18 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final user = LocalUserAccount.current.paperlessUser;
|
final user = context.read<LocalUserAccount>().paperlessUser;
|
||||||
_tabController = TabController(
|
_tabController = TabController(
|
||||||
length: _calculateTabCount(user), vsync: this)
|
length: _calculateTabCount(user), vsync: this)
|
||||||
..addListener(() => setState(() => _currentIndex = _tabController.index));
|
..addListener(() => setState(() => _currentIndex = _tabController.index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
@@ -67,22 +65,39 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
final currentUserId =
|
final currentUserId =
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||||
.getValue()!
|
.getValue()!
|
||||||
.currentLoggedInUser;
|
.loggedInUserId;
|
||||||
final user = box.get(currentUserId)!.paperlessUser;
|
final user = box.get(currentUserId)!.paperlessUser;
|
||||||
|
final fabLabel = [
|
||||||
|
S.of(context)!.addCorrespondent,
|
||||||
|
S.of(context)!.addDocumentType,
|
||||||
|
S.of(context)!.addTag,
|
||||||
|
S.of(context)!.addStoragePath,
|
||||||
|
][_currentIndex];
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectedState) {
|
builder: (context, connectedState) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: ConnectivityAwareActionWrapper(
|
||||||
onPressed: [
|
offlineBuilder: (context, child) => const SizedBox.shrink(),
|
||||||
if (user.canViewCorrespondents) _openAddCorrespondentPage,
|
child: FloatingActionButton.extended(
|
||||||
if (user.canViewDocumentTypes) _openAddDocumentTypePage,
|
heroTag: "inbox_page_fab",
|
||||||
if (user.canViewTags) _openAddTagPage,
|
label: Text(fabLabel),
|
||||||
if (user.canViewStoragePaths) _openAddStoragePathPage,
|
icon: Icon(Icons.add),
|
||||||
][_currentIndex],
|
onPressed: [
|
||||||
child: const Icon(Icons.add),
|
if (user.canViewCorrespondents)
|
||||||
|
() => CreateLabelRoute(LabelType.correspondent)
|
||||||
|
.push(context),
|
||||||
|
if (user.canViewDocumentTypes)
|
||||||
|
() => CreateLabelRoute(LabelType.documentType)
|
||||||
|
.push(context),
|
||||||
|
if (user.canViewTags)
|
||||||
|
() => CreateLabelRoute(LabelType.tag).push(context),
|
||||||
|
if (user.canViewStoragePaths)
|
||||||
|
() => CreateLabelRoute(LabelType.storagePath)
|
||||||
|
.push(context),
|
||||||
|
][_currentIndex],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
floatHeaderSlivers: true,
|
floatHeaderSlivers: true,
|
||||||
@@ -213,144 +228,13 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: [
|
children: [
|
||||||
if (user.canViewCorrespondents)
|
if (user.canViewCorrespondents)
|
||||||
Builder(
|
_buildCorrespondentsView(state, user),
|
||||||
builder: (context) {
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: searchBarHandle),
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: tabBarHandle),
|
|
||||||
LabelTabView<Correspondent>(
|
|
||||||
labels: state.correspondents,
|
|
||||||
filterBuilder: (label) =>
|
|
||||||
DocumentFilter(
|
|
||||||
correspondent:
|
|
||||||
IdQueryParameter.fromId(
|
|
||||||
label.id!),
|
|
||||||
),
|
|
||||||
canEdit: user.canEditCorrespondents,
|
|
||||||
canAddNew:
|
|
||||||
user.canCreateCorrespondents,
|
|
||||||
onEdit: _openEditCorrespondentPage,
|
|
||||||
emptyStateActionButtonLabel: S
|
|
||||||
.of(context)!
|
|
||||||
.addNewCorrespondent,
|
|
||||||
emptyStateDescription: S
|
|
||||||
.of(context)!
|
|
||||||
.noCorrespondentsSetUp,
|
|
||||||
onAddNew: _openAddCorrespondentPage,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (user.canViewDocumentTypes)
|
if (user.canViewDocumentTypes)
|
||||||
Builder(
|
_buildDocumentTypesView(state, user),
|
||||||
builder: (context) {
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: searchBarHandle),
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: tabBarHandle),
|
|
||||||
LabelTabView<DocumentType>(
|
|
||||||
labels: state.documentTypes,
|
|
||||||
filterBuilder: (label) =>
|
|
||||||
DocumentFilter(
|
|
||||||
documentType:
|
|
||||||
IdQueryParameter.fromId(
|
|
||||||
label.id!),
|
|
||||||
),
|
|
||||||
canEdit: user.canEditDocumentTypes,
|
|
||||||
canAddNew:
|
|
||||||
user.canCreateDocumentTypes,
|
|
||||||
onEdit: _openEditDocumentTypePage,
|
|
||||||
emptyStateActionButtonLabel: S
|
|
||||||
.of(context)!
|
|
||||||
.addNewDocumentType,
|
|
||||||
emptyStateDescription: S
|
|
||||||
.of(context)!
|
|
||||||
.noDocumentTypesSetUp,
|
|
||||||
onAddNew: _openAddDocumentTypePage,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (user.canViewTags)
|
if (user.canViewTags)
|
||||||
Builder(
|
_buildTagsView(state, user),
|
||||||
builder: (context) {
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: searchBarHandle),
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: tabBarHandle),
|
|
||||||
LabelTabView<Tag>(
|
|
||||||
labels: state.tags,
|
|
||||||
filterBuilder: (label) =>
|
|
||||||
DocumentFilter(
|
|
||||||
tags: TagsQuery.ids(
|
|
||||||
include: [label.id!]),
|
|
||||||
),
|
|
||||||
canEdit: user.canEditTags,
|
|
||||||
canAddNew: user.canCreateTags,
|
|
||||||
onEdit: _openEditTagPage,
|
|
||||||
leadingBuilder: (t) => CircleAvatar(
|
|
||||||
backgroundColor: t.color,
|
|
||||||
child: t.isInboxTag
|
|
||||||
? Icon(
|
|
||||||
Icons.inbox,
|
|
||||||
color: t.textColor,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
emptyStateActionButtonLabel:
|
|
||||||
S.of(context)!.addNewTag,
|
|
||||||
emptyStateDescription:
|
|
||||||
S.of(context)!.noTagsSetUp,
|
|
||||||
onAddNew: _openAddTagPage,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (user.canViewStoragePaths)
|
if (user.canViewStoragePaths)
|
||||||
Builder(
|
_buildStoragePathView(state, user),
|
||||||
builder: (context) {
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: searchBarHandle),
|
|
||||||
SliverOverlapInjector(
|
|
||||||
handle: tabBarHandle),
|
|
||||||
LabelTabView<StoragePath>(
|
|
||||||
labels: state.storagePaths,
|
|
||||||
onEdit: _openEditStoragePathPage,
|
|
||||||
filterBuilder: (label) =>
|
|
||||||
DocumentFilter(
|
|
||||||
storagePath:
|
|
||||||
IdQueryParameter.fromId(
|
|
||||||
label.id!),
|
|
||||||
),
|
|
||||||
canEdit: user.canEditStoragePaths,
|
|
||||||
canAddNew:
|
|
||||||
user.canCreateStoragePaths,
|
|
||||||
contentBuilder: (path) =>
|
|
||||||
Text(path.path),
|
|
||||||
emptyStateActionButtonLabel: S
|
|
||||||
.of(context)!
|
|
||||||
.addNewStoragePath,
|
|
||||||
emptyStateDescription: S
|
|
||||||
.of(context)!
|
|
||||||
.noStoragePathsSetUp,
|
|
||||||
onAddNew: _openAddStoragePathPage,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -365,73 +249,124 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openEditCorrespondentPage(Correspondent correspondent) {
|
Widget _buildCorrespondentsView(LabelState state, UserModel user) {
|
||||||
Navigator.push(
|
return Builder(
|
||||||
context,
|
builder: (context) {
|
||||||
_buildLabelPageRoute(EditCorrespondentPage(correspondent: correspondent)),
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverOverlapInjector(handle: searchBarHandle),
|
||||||
|
SliverOverlapInjector(handle: tabBarHandle),
|
||||||
|
LabelTabView<Correspondent>(
|
||||||
|
labels: state.correspondents,
|
||||||
|
filterBuilder: (label) => DocumentFilter(
|
||||||
|
correspondent: IdQueryParameter.fromId(label.id!),
|
||||||
|
),
|
||||||
|
canEdit: user.canEditCorrespondents,
|
||||||
|
canAddNew: user.canCreateCorrespondents,
|
||||||
|
onEdit: (correspondent) {
|
||||||
|
EditLabelRoute(correspondent).push(context);
|
||||||
|
},
|
||||||
|
emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent,
|
||||||
|
emptyStateDescription: S.of(context)!.noCorrespondentsSetUp,
|
||||||
|
onAddNew: () =>
|
||||||
|
CreateLabelRoute(LabelType.correspondent).push(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openEditDocumentTypePage(DocumentType docType) {
|
Widget _buildDocumentTypesView(LabelState state, UserModel user) {
|
||||||
Navigator.push(
|
return Builder(
|
||||||
context,
|
builder: (context) {
|
||||||
_buildLabelPageRoute(EditDocumentTypePage(documentType: docType)),
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverOverlapInjector(handle: searchBarHandle),
|
||||||
|
SliverOverlapInjector(handle: tabBarHandle),
|
||||||
|
LabelTabView<DocumentType>(
|
||||||
|
labels: state.documentTypes,
|
||||||
|
filterBuilder: (label) => DocumentFilter(
|
||||||
|
documentType: IdQueryParameter.fromId(label.id!),
|
||||||
|
),
|
||||||
|
canEdit: user.canEditDocumentTypes,
|
||||||
|
canAddNew: user.canCreateDocumentTypes,
|
||||||
|
onEdit: (label) {
|
||||||
|
EditLabelRoute(label).push(context);
|
||||||
|
},
|
||||||
|
emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType,
|
||||||
|
emptyStateDescription: S.of(context)!.noDocumentTypesSetUp,
|
||||||
|
onAddNew: () =>
|
||||||
|
CreateLabelRoute(LabelType.documentType).push(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openEditTagPage(Tag tag) {
|
Widget _buildTagsView(LabelState state, UserModel user) {
|
||||||
Navigator.push(
|
return Builder(
|
||||||
context,
|
builder: (context) {
|
||||||
_buildLabelPageRoute(EditTagPage(tag: tag)),
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverOverlapInjector(handle: searchBarHandle),
|
||||||
|
SliverOverlapInjector(handle: tabBarHandle),
|
||||||
|
LabelTabView<Tag>(
|
||||||
|
labels: state.tags,
|
||||||
|
filterBuilder: (label) => DocumentFilter(
|
||||||
|
tags: TagsQuery.ids(include: [label.id!]),
|
||||||
|
),
|
||||||
|
canEdit: user.canEditTags,
|
||||||
|
canAddNew: user.canCreateTags,
|
||||||
|
onEdit: (label) {
|
||||||
|
EditLabelRoute(label).push(context);
|
||||||
|
},
|
||||||
|
leadingBuilder: (t) => CircleAvatar(
|
||||||
|
backgroundColor: t.color,
|
||||||
|
child: t.isInboxTag
|
||||||
|
? Icon(
|
||||||
|
Icons.inbox,
|
||||||
|
color: t.textColor,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
emptyStateActionButtonLabel: S.of(context)!.addNewTag,
|
||||||
|
emptyStateDescription: S.of(context)!.noTagsSetUp,
|
||||||
|
onAddNew: () => CreateLabelRoute(LabelType.tag).push(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openEditStoragePathPage(StoragePath path) {
|
Widget _buildStoragePathView(LabelState state, UserModel user) {
|
||||||
Navigator.push(
|
return Builder(
|
||||||
context,
|
builder: (context) {
|
||||||
_buildLabelPageRoute(EditStoragePathPage(
|
return CustomScrollView(
|
||||||
storagePath: path,
|
slivers: [
|
||||||
)),
|
SliverOverlapInjector(handle: searchBarHandle),
|
||||||
);
|
SliverOverlapInjector(handle: tabBarHandle),
|
||||||
}
|
LabelTabView<StoragePath>(
|
||||||
|
labels: state.storagePaths,
|
||||||
void _openAddCorrespondentPage() {
|
onEdit: (label) {
|
||||||
Navigator.push(
|
EditLabelRoute(label).push(context);
|
||||||
context,
|
},
|
||||||
_buildLabelPageRoute(const AddCorrespondentPage()),
|
filterBuilder: (label) => DocumentFilter(
|
||||||
);
|
storagePath: IdQueryParameter.fromId(label.id!),
|
||||||
}
|
),
|
||||||
|
canEdit: user.canEditStoragePaths,
|
||||||
void _openAddDocumentTypePage() {
|
canAddNew: user.canCreateStoragePaths,
|
||||||
Navigator.push(
|
contentBuilder: (path) => Text(path.path),
|
||||||
context,
|
emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath,
|
||||||
_buildLabelPageRoute(const AddDocumentTypePage()),
|
emptyStateDescription: S.of(context)!.noStoragePathsSetUp,
|
||||||
);
|
onAddNew: () =>
|
||||||
}
|
CreateLabelRoute(LabelType.storagePath).push(context),
|
||||||
|
),
|
||||||
void _openAddTagPage() {
|
],
|
||||||
Navigator.push(
|
);
|
||||||
context,
|
},
|
||||||
_buildLabelPageRoute(const AddTagPage()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openAddStoragePathPage() {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
_buildLabelPageRoute(const AddStoragePathPage()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialPageRoute<dynamic> _buildLabelPageRoute(Widget page) {
|
|
||||||
return MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<ApiVersion>())
|
|
||||||
],
|
|
||||||
child: page,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class _FullscreenLabelFormState<T extends Label>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showFab = MediaQuery.viewInsetsOf(context).bottom == 0;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final options = _filterOptionsByQuery(_textEditingController.text);
|
final options = _filterOptionsByQuery(_textEditingController.text);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -124,6 +125,13 @@ class _FullscreenLabelFormState<T extends Label>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
floatingActionButton: showFab && widget.onCreateNewLabel != null
|
||||||
|
? FloatingActionButton(
|
||||||
|
heroTag: "fab_label_form",
|
||||||
|
onPressed: _onCreateNewLabel,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||||
|
|
||||||
class LabelItem<T extends Label> extends StatelessWidget {
|
class LabelItem<T extends Label> extends StatelessWidget {
|
||||||
final T label;
|
final T label;
|
||||||
@@ -36,14 +37,14 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildReferencedDocumentsWidget(BuildContext context) {
|
Widget _buildReferencedDocumentsWidget(BuildContext context) {
|
||||||
final canOpen = (label.documentCount ?? 0) > 0 &&
|
final canOpen = (label.documentCount ?? 0) > 0 &&
|
||||||
LocalUserAccount.current.paperlessUser.canViewDocuments;
|
context.watch<LocalUserAccount>().paperlessUser.canViewDocuments;
|
||||||
return TextButton.icon(
|
return TextButton.icon(
|
||||||
label: const Icon(Icons.link),
|
label: const Icon(Icons.link),
|
||||||
icon: Text(formatMaxCount(label.documentCount)),
|
icon: Text(formatMaxCount(label.documentCount)),
|
||||||
onPressed: canOpen
|
onPressed: canOpen
|
||||||
? () {
|
? () {
|
||||||
final filter = filterBuilder(label);
|
final filter = filterBuilder(label);
|
||||||
pushLinkedDocumentsView(context, filter: filter);
|
LinkedDocumentsRoute(filter).push(context);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
|||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
if (!connectivityState.isConnected) {
|
if (!connectivityState.isConnected) {
|
||||||
return const OfflineWidget();
|
return const SliverFillRemaining(child: OfflineWidget());
|
||||||
}
|
}
|
||||||
final sortedLabels = labels.values.toList()..sort();
|
final sortedLabels = labels.values.toList()..sort();
|
||||||
if (labels.isEmpty) {
|
if (labels.isEmpty) {
|
||||||
@@ -76,9 +76,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
translateMatchingAlgorithmName(
|
translateMatchingAlgorithmName(
|
||||||
context, l.matchingAlgorithm) +
|
context, l.matchingAlgorithm) +
|
||||||
((l.match?.isNotEmpty ?? false)
|
(l.match.isNotEmpty ? ": ${l.match}" : ""),
|
||||||
? ": ${l.match}"
|
|
||||||
: ""),
|
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
onOpenEditPage: canEdit ? onEdit : null,
|
onOpenEditPage: canEdit ? onEdit : null,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class LabelText<T extends Label> extends StatelessWidget {
|
|||||||
const LabelText({
|
const LabelText({
|
||||||
super.key,
|
super.key,
|
||||||
this.style,
|
this.style,
|
||||||
this.placeholder = "",
|
this.placeholder = "-",
|
||||||
required this.label,
|
required this.label,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
||||||
|
import 'package:paperless_mobile/features/landing/view/widgets/expansion_card.dart';
|
||||||
|
import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view_details/view/saved_view_preview.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/branches/inbox_route.dart';
|
||||||
|
|
||||||
|
class LandingPage extends StatefulWidget {
|
||||||
|
const LandingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LandingPage> createState() => _LandingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LandingPageState extends State<LandingPage> {
|
||||||
|
final _searchBarHandle = SliverOverlapAbsorberHandle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||||
|
return SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
drawer: const AppDrawer(),
|
||||||
|
body: NestedScrollView(
|
||||||
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
SliverOverlapAbsorber(
|
||||||
|
handle: _searchBarHandle,
|
||||||
|
sliver: SliverSearchBar(
|
||||||
|
floating: true,
|
||||||
|
titleText: S.of(context)!.documents,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
S.of(context)!.welcomeUser(
|
||||||
|
currentUser.fullName ?? currentUser.username),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.displaySmall
|
||||||
|
?.copyWith(fontSize: 28),
|
||||||
|
).padded(24),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(child: _buildStatisticsCard(context)),
|
||||||
|
if (currentUser.canViewSavedViews) ...[
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 0, 8),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.saved_search,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
).paddedOnly(right: 8),
|
||||||
|
Text(
|
||||||
|
S.of(context)!.views,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return state.maybeWhen(
|
||||||
|
loaded: (savedViews) {
|
||||||
|
final dashboardViews = savedViews.values
|
||||||
|
.where((element) => element.showOnDashboard)
|
||||||
|
.toList();
|
||||||
|
if (dashboardViews.isEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(S.of(context)!.noSavedViewOnHomepageHint)
|
||||||
|
.padded(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(S.of(context)!.newView),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddedOnly(left: 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SliverList.builder(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return SavedViewPreview(
|
||||||
|
savedView: dashboardViews.elementAt(index),
|
||||||
|
expanded: index == 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: dashboardViews.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
orElse: () => const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatisticsCard(BuildContext context) {
|
||||||
|
final currentUser = context.read<LocalUserAccount>().paperlessUser;
|
||||||
|
return ExpansionCard(
|
||||||
|
initiallyExpanded: false,
|
||||||
|
title: Text(
|
||||||
|
S.of(context)!.statistics,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
content: FutureBuilder<PaperlessServerStatisticsModel>(
|
||||||
|
future: context.read<PaperlessServerStatsApi>().getServerStatistics(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
).paddedOnly(top: 8, bottom: 24);
|
||||||
|
}
|
||||||
|
final stats = snapshot.data!;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
child: ListTile(
|
||||||
|
shape: Theme.of(context).cardTheme.shape,
|
||||||
|
titleTextStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
|
title: Text(S.of(context)!.documentsInInbox),
|
||||||
|
onTap: currentUser.canViewInbox
|
||||||
|
? () => InboxRoute().go(context)
|
||||||
|
: null,
|
||||||
|
trailing: Text(
|
||||||
|
stats.documentsInInbox.toString(),
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
child: ListTile(
|
||||||
|
shape: Theme.of(context).cardTheme.shape,
|
||||||
|
titleTextStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
|
title: Text(S.of(context)!.totalDocuments),
|
||||||
|
onTap: currentUser.canViewDocuments
|
||||||
|
? () {
|
||||||
|
DocumentsRoute().go(context);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
trailing: Text(
|
||||||
|
stats.documentsTotal.toString(),
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
child: ListTile(
|
||||||
|
shape: Theme.of(context).cardTheme.shape,
|
||||||
|
titleTextStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
|
title: Text(S.of(context)!.totalCharacters),
|
||||||
|
trailing: Text(
|
||||||
|
stats.totalChars.toString(),
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1.3,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: MimeTypesPieChart(statistics: stats),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(16);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user