mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-12 20:12:22 -06:00
Merge branch 'feature/reworked-settings-and-user-management' into development
This commit is contained in:
37
.github/workflows/release_deploy_play_store.yml
vendored
37
.github/workflows/release_deploy_play_store.yml
vendored
@@ -11,6 +11,8 @@ on:
|
|||||||
default: "alpha"
|
default: "alpha"
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
|
- internal
|
||||||
|
- promote_to_alpha
|
||||||
- alpha
|
- alpha
|
||||||
- promote_to_beta
|
- promote_to_beta
|
||||||
- beta
|
- beta
|
||||||
@@ -31,25 +33,15 @@ jobs:
|
|||||||
channel: stable
|
channel: stable
|
||||||
- run: flutter doctor -v
|
- run: flutter doctor -v
|
||||||
|
|
||||||
# Setup app
|
# Clone repository
|
||||||
- name: Checkout Paperless mobile, get packages and run code generators
|
- name: Checkout Paperless mobile, get packages and run code generators
|
||||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
|
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
|
||||||
- run: |
|
|
||||||
pushd scripts
|
|
||||||
bash install_dependencies.sh
|
|
||||||
popd
|
|
||||||
|
|
||||||
# Setup Ruby, Bundler, and Gemfile dependencies
|
|
||||||
- name: Setup Fastlane
|
|
||||||
uses: ruby/setup-ruby@8df78e55761745aad83acaf3ff12976382356e6d
|
|
||||||
with:
|
|
||||||
ruby-version: "2.6"
|
|
||||||
bundler-cache: true
|
|
||||||
working-directory: android
|
|
||||||
|
|
||||||
|
# Extract secrets into corresponding files
|
||||||
- name: Configure Keystore
|
- name: Configure Keystore
|
||||||
run: |
|
run: |
|
||||||
echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > app/upload-keystore.jks
|
echo "$RELEASE_KEYSTORE" > upload-keystore.jks.asc
|
||||||
|
gpg --batch --passphrase "$RELEASE_KEYSTORE_PASSPHRASE" -d -o app/upload-keystore.jks upload-keystore.jks.asc
|
||||||
echo "storeFile=upload-keystore.jks" >> key.properties
|
echo "storeFile=upload-keystore.jks" >> key.properties
|
||||||
echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> key.properties
|
echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> key.properties
|
||||||
echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> key.properties
|
echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> key.properties
|
||||||
@@ -59,8 +51,25 @@ jobs:
|
|||||||
KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }}
|
KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }}
|
||||||
KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
|
KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
|
||||||
KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
|
KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
|
||||||
|
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
|
||||||
|
RELEASE_KEYSTORE_PASSPHRASE: ${{ secrets.RELEASE_KEYSTORE_PASSPHRASE }}
|
||||||
working-directory: android
|
working-directory: android
|
||||||
|
|
||||||
|
# Run codegen
|
||||||
|
- name: Run Codegen
|
||||||
|
run: |
|
||||||
|
bash install_dependencies.sh
|
||||||
|
working-directory: scripts
|
||||||
|
|
||||||
|
# Setup Ruby, Bundler, and Gemfile dependencies
|
||||||
|
- name: Setup Fastlane
|
||||||
|
uses: ruby/setup-ruby@8df78e55761745aad83acaf3ff12976382356e6d
|
||||||
|
with:
|
||||||
|
ruby-version: "2.6"
|
||||||
|
bundler-cache: true
|
||||||
|
working-directory: android
|
||||||
|
|
||||||
|
|
||||||
# Build and deploy with Fastlane (by default, to alpha track) 🚀.
|
# Build and deploy with Fastlane (by default, to alpha track) 🚀.
|
||||||
# Naturally, promote_to_production only deploys.
|
# Naturally, promote_to_production only deploys.
|
||||||
- run: bundle exec fastlane ${{ github.event.inputs.lane || 'alpha' }}
|
- run: bundle exec fastlane ${{ github.event.inputs.lane || 'alpha' }}
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ subprojects {
|
|||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ platform :android do
|
|||||||
gradle(task: "test")
|
gradle(task: "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Submit a new internal build to Google Play"
|
||||||
|
lane :internal do
|
||||||
|
sh "flutter build appbundle -v"
|
||||||
|
upload_to_play_store(
|
||||||
|
track: 'internal',
|
||||||
|
aab: '../build/app/outputs/bundle/release/app-release.aab',
|
||||||
|
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
||||||
|
release_status: "draft",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
desc "Promote internal track to alpha"
|
||||||
|
lane :promote_to_alpha do
|
||||||
|
upload_to_play_store(
|
||||||
|
track: 'internal',
|
||||||
|
track_promote_to: 'alpha',
|
||||||
|
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
desc "Submit a new alpha build to Google Play"
|
desc "Submit a new alpha build to Google Play"
|
||||||
lane :alpha do
|
lane :alpha do
|
||||||
sh "flutter build appbundle -v"
|
sh "flutter build appbundle -v"
|
||||||
@@ -28,6 +49,7 @@ platform :android do
|
|||||||
track: 'alpha',
|
track: 'alpha',
|
||||||
aab: '../build/app/outputs/bundle/release/app-release.aab',
|
aab: '../build/app/outputs/bundle/release/app-release.aab',
|
||||||
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
||||||
|
release_status: "draft",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -36,7 +58,6 @@ platform :android do
|
|||||||
upload_to_play_store(
|
upload_to_play_store(
|
||||||
track: 'alpha',
|
track: 'alpha',
|
||||||
track_promote_to: 'beta',
|
track_promote_to: 'beta',
|
||||||
skip_upload_changelogs: true,
|
|
||||||
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -48,6 +69,7 @@ platform :android do
|
|||||||
track: 'beta',
|
track: 'beta',
|
||||||
aab: '../build/app/outputs/bundle/release/app-release.aab',
|
aab: '../build/app/outputs/bundle/release/app-release.aab',
|
||||||
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
||||||
|
release_status: "draft",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,7 +78,6 @@ platform :android do
|
|||||||
upload_to_play_store(
|
upload_to_play_store(
|
||||||
track: 'beta',
|
track: 'beta',
|
||||||
track_promote_to: 'production',
|
track_promote_to: 'production',
|
||||||
skip_upload_changelogs: true,
|
|
||||||
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -68,6 +89,7 @@ platform :android do
|
|||||||
track: 'production',
|
track: 'production',
|
||||||
aab: '../build/app/outputs/bundle/release/app-release.aab',
|
aab: '../build/app/outputs/bundle/release/app-release.aab',
|
||||||
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
json_key_data: ENV['PLAY_STORE_CREDENTIALS'],
|
||||||
|
release_status: "draft",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Diese Version ist eine Betaversion und enthält neue Features und Umstrukturierungen. Deshalb wird empfohlen, die App neu zu installieren!
|
||||||
|
* Neues Feature: Massenbearbeitung von Dokumenten.
|
||||||
|
* Neues Feature: Unterstützung für mehrere Accounts und mehrere Instanzen mit schnellem Wechsel zwischen diesen (Achtung: Die in paperless-ngx 1.14.0 neu hinzugefügten Berechtigungen sind hier noch nicht enthalten!)
|
||||||
|
* Bugfixes
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
This version is a beta and contains new features and some restructurings. Therefore it is highly recommended to perform a fresh installation of the app.
|
||||||
|
* New feature: Document bulk edits
|
||||||
|
* New feature: Support for multiple accounts and multiple instances with quick switching between them (Note: This does not yet include the new multi-user features introduced in paperless-ngx 1.14.0!)
|
||||||
|
* Bugfixes
|
||||||
@@ -54,6 +54,8 @@ PODS:
|
|||||||
- 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):
|
||||||
@@ -99,16 +101,17 @@ DEPENDENCIES:
|
|||||||
- 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`)
|
||||||
- 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/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||||
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- 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`)
|
||||||
|
|
||||||
@@ -142,6 +145,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/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:
|
||||||
@@ -151,7 +156,7 @@ 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/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||||
pdfx:
|
pdfx:
|
||||||
:path: ".symlinks/plugins/pdfx/ios"
|
:path: ".symlinks/plugins/pdfx/ios"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
@@ -161,7 +166,7 @@ EXTERNAL SOURCES:
|
|||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
: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:
|
||||||
@@ -180,6 +185,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||||
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
||||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||||
@@ -195,7 +201,7 @@ SPEC CHECKSUMS:
|
|||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
|
||||||
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13
|
PODFILE CHECKSUM: 7daa35cc908d9fba025075df27cb57a1ba1ebf13
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class BlocChangesObserver extends BlocObserver {
|
|
||||||
@override
|
|
||||||
void onChange(BlocBase bloc, Change change) {
|
|
||||||
super.onChange(bloc, change);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
|
||||||
|
|
||||||
class PaperlessServerInformationCubit
|
|
||||||
extends Cubit<PaperlessServerInformationState> {
|
|
||||||
final PaperlessServerStatsApi _api;
|
|
||||||
|
|
||||||
PaperlessServerInformationCubit(this._api)
|
|
||||||
: super(PaperlessServerInformationState());
|
|
||||||
|
|
||||||
Future<void> updateInformtion() async {
|
|
||||||
final information = await _api.getServerInformation();
|
|
||||||
emit(PaperlessServerInformationState(
|
|
||||||
isLoaded: true,
|
|
||||||
information: information,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
lib/core/bloc/server_information_cubit.dart
Normal file
17
lib/core/bloc/server_information_cubit.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/server_information_state.dart';
|
||||||
|
|
||||||
|
class ServerInformationCubit extends Cubit<ServerInformationState> {
|
||||||
|
final PaperlessServerStatsApi _api;
|
||||||
|
|
||||||
|
ServerInformationCubit(this._api) : super(ServerInformationState());
|
||||||
|
|
||||||
|
Future<void> updateInformation() async {
|
||||||
|
final information = await _api.getServerInformation();
|
||||||
|
emit(ServerInformationState(
|
||||||
|
isLoaded: true,
|
||||||
|
information: information,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
class PaperlessServerInformationState {
|
class ServerInformationState {
|
||||||
final bool isLoaded;
|
final bool isLoaded;
|
||||||
final PaperlessServerInformationModel? information;
|
final PaperlessServerInformationModel? information;
|
||||||
|
|
||||||
PaperlessServerInformationState({
|
ServerInformationState({
|
||||||
this.isLoaded = false,
|
this.isLoaded = false,
|
||||||
this.information,
|
this.information,
|
||||||
});
|
});
|
||||||
47
lib/core/config/hive/custom_adapters/theme_mode_adapter.dart
Normal file
47
lib/core/config/hive/custom_adapters/theme_mode_adapter.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
|
||||||
|
class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
|
||||||
|
@override
|
||||||
|
final int typeId = HiveTypeIds.themeMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeMode read(BinaryReader reader) {
|
||||||
|
switch (reader.readByte()) {
|
||||||
|
case 0:
|
||||||
|
return ThemeMode.system;
|
||||||
|
case 1:
|
||||||
|
return ThemeMode.dark;
|
||||||
|
case 2:
|
||||||
|
return ThemeMode.light;
|
||||||
|
default:
|
||||||
|
return ThemeMode.system;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, ThemeMode obj) {
|
||||||
|
switch (obj) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
writer.writeByte(0);
|
||||||
|
break;
|
||||||
|
case ThemeMode.light:
|
||||||
|
writer.writeByte(1);
|
||||||
|
break;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
writer.writeByte(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is ThemeModeAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
59
lib/core/config/hive/hive_config.dart
Normal file
59
lib/core/config/hive/hive_config.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/custom_adapters/theme_mode_adapter.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/database/tables/user_credentials.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
|
class HiveBoxes {
|
||||||
|
HiveBoxes._();
|
||||||
|
static const globalSettings = 'globalSettings';
|
||||||
|
static const authentication = 'authentication';
|
||||||
|
static const localUserCredentials = 'localUserCredentials';
|
||||||
|
static const localUserAccount = 'localUserAccount';
|
||||||
|
static const localUserAppState = 'localUserAppState';
|
||||||
|
static const localUserSettings = 'localUserSettings';
|
||||||
|
}
|
||||||
|
|
||||||
|
class HiveTypeIds {
|
||||||
|
HiveTypeIds._();
|
||||||
|
static const globalSettings = 0;
|
||||||
|
static const localUserSettings = 1;
|
||||||
|
static const themeMode = 2;
|
||||||
|
static const colorSchemeOption = 3;
|
||||||
|
static const authentication = 4;
|
||||||
|
static const clientCertificate = 5;
|
||||||
|
static const localUserCredentials = 6;
|
||||||
|
static const localUserAccount = 7;
|
||||||
|
static const localUserAppState = 8;
|
||||||
|
static const viewType = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerHiveAdapters() {
|
||||||
|
registerPaperlessApiHiveTypeAdapters();
|
||||||
|
Hive.registerAdapter(ColorSchemeOptionAdapter());
|
||||||
|
Hive.registerAdapter(ThemeModeAdapter());
|
||||||
|
Hive.registerAdapter(GlobalSettingsAdapter());
|
||||||
|
Hive.registerAdapter(AuthenticationInformationAdapter());
|
||||||
|
Hive.registerAdapter(ClientCertificateAdapter());
|
||||||
|
Hive.registerAdapter(LocalUserSettingsAdapter());
|
||||||
|
Hive.registerAdapter(UserCredentialsAdapter());
|
||||||
|
Hive.registerAdapter(LocalUserAccountAdapter());
|
||||||
|
Hive.registerAdapter(LocalUserAppStateAdapter());
|
||||||
|
Hive.registerAdapter(ViewTypeAdapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HiveSingleValueBox<T> on Box<T> {
|
||||||
|
static const _valueKey = 'SINGLE_VALUE';
|
||||||
|
bool get hasValue => containsKey(_valueKey);
|
||||||
|
|
||||||
|
T? getValue() => get(_valueKey);
|
||||||
|
|
||||||
|
Future<void> setValue(T value) => put(_valueKey, value);
|
||||||
|
}
|
||||||
32
lib/core/database/tables/global_settings.dart
Normal file
32
lib/core/database/tables/global_settings.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
|
||||||
|
part 'global_settings.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.globalSettings)
|
||||||
|
class GlobalSettings with HiveObjectMixin {
|
||||||
|
@HiveField(0)
|
||||||
|
String preferredLocaleSubtag;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
ThemeMode preferredThemeMode;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
ColorSchemeOption preferredColorSchemeOption;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
bool showOnboarding;
|
||||||
|
|
||||||
|
@HiveField(4)
|
||||||
|
String? currentLoggedInUser;
|
||||||
|
|
||||||
|
GlobalSettings({
|
||||||
|
required this.preferredLocaleSubtag,
|
||||||
|
this.preferredThemeMode = ThemeMode.system,
|
||||||
|
this.preferredColorSchemeOption = ColorSchemeOption.classic,
|
||||||
|
this.showOnboarding = true,
|
||||||
|
this.currentLoggedInUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
31
lib/core/database/tables/local_user_account.dart
Normal file
31
lib/core/database/tables/local_user_account.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:hive_flutter/adapters.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';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.localUserAccount)
|
||||||
|
class LocalUserAccount extends HiveObject {
|
||||||
|
@HiveField(0)
|
||||||
|
final String serverUrl;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
final String username;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
final String? fullName;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@HiveField(4)
|
||||||
|
LocalUserSettings settings;
|
||||||
|
|
||||||
|
LocalUserAccount({
|
||||||
|
required this.id,
|
||||||
|
required this.serverUrl,
|
||||||
|
required this.username,
|
||||||
|
required this.settings,
|
||||||
|
this.fullName,
|
||||||
|
});
|
||||||
|
}
|
||||||
40
lib/core/database/tables/local_user_app_state.dart
Normal file
40
lib/core/database/tables/local_user_app_state.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
|
part 'local_user_app_state.g.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Object used for the persistence of app state, e.g. set filters,
|
||||||
|
/// search history and implicit settings.
|
||||||
|
///
|
||||||
|
@HiveType(typeId: HiveTypeIds.localUserAppState)
|
||||||
|
class LocalUserAppState extends HiveObject {
|
||||||
|
@HiveField(0)
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
DocumentFilter currentDocumentFilter;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
List<String> documentSearchHistory;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
ViewType documentsPageViewType;
|
||||||
|
|
||||||
|
@HiveField(4)
|
||||||
|
ViewType savedViewsViewType;
|
||||||
|
|
||||||
|
@HiveField(5)
|
||||||
|
ViewType documentSearchViewType;
|
||||||
|
|
||||||
|
LocalUserAppState({
|
||||||
|
required this.userId,
|
||||||
|
this.currentDocumentFilter = const DocumentFilter(),
|
||||||
|
this.documentSearchHistory = const [],
|
||||||
|
this.documentsPageViewType = ViewType.list,
|
||||||
|
this.documentSearchViewType = ViewType.list,
|
||||||
|
this.savedViewsViewType = ViewType.list,
|
||||||
|
});
|
||||||
|
}
|
||||||
14
lib/core/database/tables/local_user_settings.dart
Normal file
14
lib/core/database/tables/local_user_settings.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
|
||||||
|
part 'local_user_settings.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.localUserSettings)
|
||||||
|
class LocalUserSettings with HiveObjectMixin {
|
||||||
|
@HiveField(0)
|
||||||
|
bool isBiometricAuthenticationEnabled;
|
||||||
|
|
||||||
|
LocalUserSettings({
|
||||||
|
this.isBiometricAuthenticationEnabled = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
18
lib/core/database/tables/user_credentials.dart
Normal file
18
lib/core/database/tables/user_credentials.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
|
||||||
|
part 'user_credentials.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.localUserCredentials)
|
||||||
|
class UserCredentials extends HiveObject {
|
||||||
|
@HiveField(0)
|
||||||
|
final String token;
|
||||||
|
@HiveField(1)
|
||||||
|
final ClientCertificate? clientCertificate;
|
||||||
|
|
||||||
|
UserCredentials({
|
||||||
|
required this.token,
|
||||||
|
this.clientCertificate,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -23,8 +23,8 @@ class DocumentChangedNotifier {
|
|||||||
_deleted.add(deleted);
|
_deleted.add(deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
void subscribe(
|
void addListener(
|
||||||
dynamic subscriber, {
|
Object subscriber, {
|
||||||
DocumentChangedCallback? onUpdated,
|
DocumentChangedCallback? onUpdated,
|
||||||
DocumentChangedCallback? onDeleted,
|
DocumentChangedCallback? onDeleted,
|
||||||
}) {
|
}) {
|
||||||
@@ -41,7 +41,7 @@ class DocumentChangedNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void unsubscribe(dynamic subscriber) {
|
void removeListener(Object subscriber) {
|
||||||
_subscribers[subscriber]?.forEach((element) {
|
_subscribers[subscriber]?.forEach((element) {
|
||||||
element.cancel();
|
element.cancel();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
import 'package:rxdart/subjects.dart';
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Base repository class which all repositories should implement
|
|
||||||
///
|
|
||||||
abstract class BaseRepository<T> extends Cubit<IndexedRepositoryState<T>>
|
|
||||||
with HydratedMixin {
|
|
||||||
final IndexedRepositoryState<T> _initialState;
|
|
||||||
|
|
||||||
BaseRepository(this._initialState) : super(_initialState) {
|
|
||||||
hydrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<IndexedRepositoryState<T>?> get values =>
|
|
||||||
BehaviorSubject.seeded(state)..addStream(super.stream);
|
|
||||||
|
|
||||||
IndexedRepositoryState<T>? get current => state;
|
|
||||||
|
|
||||||
bool get isInitialized => state.hasLoaded;
|
|
||||||
|
|
||||||
Future<T> create(T object);
|
|
||||||
Future<T?> find(int id);
|
|
||||||
Future<Iterable<T>> findAll([Iterable<int>? ids]);
|
|
||||||
Future<T> update(T object);
|
|
||||||
Future<int> delete(T object);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> clear() async {
|
|
||||||
await super.clear();
|
|
||||||
emit(_initialState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
|
||||||
|
|
||||||
class CorrespondentRepositoryImpl extends LabelRepository<Correspondent> {
|
|
||||||
final PaperlessLabelsApi _api;
|
|
||||||
|
|
||||||
CorrespondentRepositoryImpl(this._api)
|
|
||||||
: super(const CorrespondentRepositoryState());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Correspondent> create(Correspondent correspondent) async {
|
|
||||||
final created = await _api.saveCorrespondent(correspondent);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> delete(Correspondent correspondent) async {
|
|
||||||
await _api.deleteCorrespondent(correspondent);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..removeWhere((k, v) => k == correspondent.id);
|
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return correspondent.id!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Correspondent?> find(int id) async {
|
|
||||||
final correspondent = await _api.getCorrespondent(id);
|
|
||||||
if (correspondent != null) {
|
|
||||||
final updatedState = {...state.values ?? {}}..[id] = correspondent;
|
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return correspondent;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
|
|
||||||
final correspondents = await _api.getCorrespondents(ids);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
|
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return correspondents;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Correspondent> update(Correspondent correspondent) async {
|
|
||||||
final updated = await _api.updateCorrespondent(correspondent);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..update(updated.id!, (_) => updated);
|
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
CorrespondentRepositoryState fromJson(Map<String, dynamic> json) {
|
|
||||||
return CorrespondentRepositoryState.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson(covariant CorrespondentRepositoryState state) {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
|
||||||
|
|
||||||
class DocumentTypeRepositoryImpl extends LabelRepository<DocumentType> {
|
|
||||||
final PaperlessLabelsApi _api;
|
|
||||||
|
|
||||||
DocumentTypeRepositoryImpl(this._api)
|
|
||||||
: super(const DocumentTypeRepositoryState());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DocumentType> create(DocumentType documentType) async {
|
|
||||||
final created = await _api.saveDocumentType(documentType);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> delete(DocumentType documentType) async {
|
|
||||||
await _api.deleteDocumentType(documentType);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..removeWhere((k, v) => k == documentType.id);
|
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return documentType.id!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DocumentType?> find(int id) async {
|
|
||||||
final documentType = await _api.getDocumentType(id);
|
|
||||||
if (documentType != null) {
|
|
||||||
final updatedState = {...state.values ?? {}}..[id] = documentType;
|
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return documentType;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
|
|
||||||
final documentTypes = await _api.getDocumentTypes(ids);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
|
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return documentTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DocumentType> update(DocumentType documentType) async {
|
|
||||||
final updated = await _api.updateDocumentType(documentType);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..update(updated.id!, (_) => updated);
|
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DocumentTypeRepositoryState fromJson(Map<String, dynamic> json) {
|
|
||||||
return DocumentTypeRepositoryState.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson(covariant DocumentTypeRepositoryState state) {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
|
|
||||||
|
|
||||||
class SavedViewRepositoryImpl extends SavedViewRepository {
|
|
||||||
final PaperlessSavedViewsApi _api;
|
|
||||||
|
|
||||||
SavedViewRepositoryImpl(this._api) : super(const SavedViewRepositoryState());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<SavedView> create(SavedView object) async {
|
|
||||||
final created = await _api.save(object);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> delete(SavedView view) async {
|
|
||||||
await _api.delete(view);
|
|
||||||
final updatedState = {...state.values ?? {}}..remove(view.id);
|
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return view.id!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<SavedView?> find(int id) async {
|
|
||||||
final found = await _api.find(id);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..update(id, (_) => found, ifAbsent: () => found);
|
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
|
|
||||||
final found = await _api.findAll(ids);
|
|
||||||
final updatedState = {
|
|
||||||
...state.values ?? {},
|
|
||||||
...{for (final view in found) view.id!: view},
|
|
||||||
};
|
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<SavedView> update(SavedView object) {
|
|
||||||
throw UnimplementedError(
|
|
||||||
"Saved view update is not yet implemented as it is not supported by the API.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SavedViewRepositoryState fromJson(Map<String, dynamic> json) {
|
|
||||||
return SavedViewRepositoryState.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson(covariant SavedViewRepositoryState state) {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
|
||||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
|
||||||
|
|
||||||
class StoragePathRepositoryImpl extends LabelRepository<StoragePath> {
|
|
||||||
final PaperlessLabelsApi _api;
|
|
||||||
|
|
||||||
StoragePathRepositoryImpl(this._api)
|
|
||||||
: super(const StoragePathRepositoryState());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<StoragePath> create(StoragePath storagePath) async {
|
|
||||||
final created = await _api.saveStoragePath(storagePath);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> delete(StoragePath storagePath) async {
|
|
||||||
await _api.deleteStoragePath(storagePath);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..removeWhere((k, v) => k == storagePath.id);
|
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return storagePath.id!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<StoragePath?> find(int id) async {
|
|
||||||
final storagePath = await _api.getStoragePath(id);
|
|
||||||
if (storagePath != null) {
|
|
||||||
final updatedState = {...state.values ?? {}}..[id] = storagePath;
|
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return storagePath;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
|
|
||||||
final storagePaths = await _api.getStoragePaths(ids);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
|
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return storagePaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<StoragePath> update(StoragePath storagePath) async {
|
|
||||||
final updated = await _api.updateStoragePath(storagePath);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..update(updated.id!, (_) => updated);
|
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
StoragePathRepositoryState fromJson(Map<String, dynamic> json) {
|
|
||||||
return StoragePathRepositoryState.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson(covariant StoragePathRepositoryState state) {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
|
||||||
|
|
||||||
class TagRepositoryImpl extends LabelRepository<Tag> {
|
|
||||||
final PaperlessLabelsApi _api;
|
|
||||||
|
|
||||||
TagRepositoryImpl(this._api) : super(const TagRepositoryState());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Tag> create(Tag object) async {
|
|
||||||
final created = await _api.saveTag(object);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..putIfAbsent(created.id!, () => created);
|
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> delete(Tag tag) async {
|
|
||||||
await _api.deleteTag(tag);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..removeWhere((k, v) => k == tag.id);
|
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return tag.id!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Tag?> find(int id) async {
|
|
||||||
final tag = await _api.getTag(id);
|
|
||||||
if (tag != null) {
|
|
||||||
final updatedState = {...state.values ?? {}}..[id] = tag;
|
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
|
|
||||||
final tags = await _api.getTags(ids);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
|
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Tag> update(Tag tag) async {
|
|
||||||
final updated = await _api.updateTag(tag);
|
|
||||||
final updatedState = {...state.values ?? {}}
|
|
||||||
..update(updated.id!, (_) => updated);
|
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TagRepositoryState? fromJson(Map<String, dynamic> json) {
|
|
||||||
return TagRepositoryState.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic>? toJson(covariant TagRepositoryState state) {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,221 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'dart:async';
|
||||||
import 'package:paperless_mobile/core/repository/base_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
|
|
||||||
abstract class LabelRepository<T extends Label> extends BaseRepository<T> {
|
import 'package:flutter/widgets.dart';
|
||||||
LabelRepository(IndexedRepositoryState<T> initial) : super(initial);
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
|
||||||
|
|
||||||
|
class LabelRepository extends HydratedCubit<LabelRepositoryState> {
|
||||||
|
final PaperlessLabelsApi _api;
|
||||||
|
final Map<Object, StreamSubscription> _subscribers = {};
|
||||||
|
|
||||||
|
LabelRepository(this._api) : super(const LabelRepositoryState());
|
||||||
|
|
||||||
|
void addListener(
|
||||||
|
Object source, {
|
||||||
|
required void Function(LabelRepositoryState) onChanged,
|
||||||
|
}) {
|
||||||
|
onChanged(state);
|
||||||
|
_subscribers.putIfAbsent(source, () {
|
||||||
|
return stream.listen((event) => onChanged(event));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeListener(Object source) async {
|
||||||
|
await _subscribers[source]?.cancel();
|
||||||
|
_subscribers.remove(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initialize() {
|
||||||
|
debugPrint("Initializing labels...");
|
||||||
|
return Future.wait([
|
||||||
|
findAllCorrespondents(),
|
||||||
|
findAllDocumentTypes(),
|
||||||
|
findAllStoragePaths(),
|
||||||
|
findAllTags(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Tag> createTag(Tag object) async {
|
||||||
|
final created = await _api.saveTag(object);
|
||||||
|
final updatedState = {...state.tags}..putIfAbsent(created.id!, () => created);
|
||||||
|
emit(state.copyWith(tags: updatedState));
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteTag(Tag tag) async {
|
||||||
|
await _api.deleteTag(tag);
|
||||||
|
final updatedState = {...state.tags}..removeWhere((k, v) => k == tag.id);
|
||||||
|
emit(state.copyWith(tags: updatedState));
|
||||||
|
return tag.id!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Tag?> findTag(int id) async {
|
||||||
|
final tag = await _api.getTag(id);
|
||||||
|
if (tag != null) {
|
||||||
|
final updatedState = {...state.tags}..[id] = tag;
|
||||||
|
emit(state.copyWith(tags: updatedState));
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
|
||||||
|
final tags = await _api.getTags(ids);
|
||||||
|
final updatedState = {...state.tags}..addEntries(tags.map((e) => MapEntry(e.id!, e)));
|
||||||
|
emit(state.copyWith(tags: updatedState));
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Tag> updateTag(Tag tag) async {
|
||||||
|
final updated = await _api.updateTag(tag);
|
||||||
|
final updatedState = {...state.tags}..update(updated.id!, (_) => updated);
|
||||||
|
emit(state.copyWith(tags: updatedState));
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
|
||||||
|
final created = await _api.saveCorrespondent(correspondent);
|
||||||
|
final updatedState = {...state.correspondents}..putIfAbsent(created.id!, () => created);
|
||||||
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteCorrespondent(Correspondent correspondent) async {
|
||||||
|
await _api.deleteCorrespondent(correspondent);
|
||||||
|
final updatedState = {...state.correspondents}..removeWhere((k, v) => k == correspondent.id);
|
||||||
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
|
|
||||||
|
return correspondent.id!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Correspondent?> findCorrespondent(int id) async {
|
||||||
|
final correspondent = await _api.getCorrespondent(id);
|
||||||
|
if (correspondent != null) {
|
||||||
|
final updatedState = {...state.correspondents}..[id] = correspondent;
|
||||||
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
|
|
||||||
|
return correspondent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Iterable<Correspondent>> findAllCorrespondents([Iterable<int>? ids]) async {
|
||||||
|
final correspondents = await _api.getCorrespondents(ids);
|
||||||
|
final updatedState = {...state.correspondents}
|
||||||
|
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
|
||||||
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
|
|
||||||
|
return correspondents;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
|
||||||
|
final updated = await _api.updateCorrespondent(correspondent);
|
||||||
|
final updatedState = {...state.correspondents}..update(updated.id!, (_) => updated);
|
||||||
|
emit(state.copyWith(correspondents: updatedState));
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DocumentType> createDocumentType(DocumentType documentType) async {
|
||||||
|
final created = await _api.saveDocumentType(documentType);
|
||||||
|
final updatedState = {...state.documentTypes}..putIfAbsent(created.id!, () => created);
|
||||||
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteDocumentType(DocumentType documentType) async {
|
||||||
|
await _api.deleteDocumentType(documentType);
|
||||||
|
final updatedState = {...state.documentTypes}..removeWhere((k, v) => k == documentType.id);
|
||||||
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
|
return documentType.id!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DocumentType?> findDocumentType(int id) async {
|
||||||
|
final documentType = await _api.getDocumentType(id);
|
||||||
|
if (documentType != null) {
|
||||||
|
final updatedState = {...state.documentTypes}..[id] = documentType;
|
||||||
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
|
return documentType;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Iterable<DocumentType>> findAllDocumentTypes([Iterable<int>? ids]) async {
|
||||||
|
final documentTypes = await _api.getDocumentTypes(ids);
|
||||||
|
final updatedState = {...state.documentTypes}
|
||||||
|
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
|
||||||
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
|
return documentTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
|
||||||
|
final updated = await _api.updateDocumentType(documentType);
|
||||||
|
final updatedState = {...state.documentTypes}..update(updated.id!, (_) => updated);
|
||||||
|
emit(state.copyWith(documentTypes: updatedState));
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StoragePath> createStoragePath(StoragePath storagePath) async {
|
||||||
|
final created = await _api.saveStoragePath(storagePath);
|
||||||
|
final updatedState = {...state.storagePaths}..putIfAbsent(created.id!, () => created);
|
||||||
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteStoragePath(StoragePath storagePath) async {
|
||||||
|
await _api.deleteStoragePath(storagePath);
|
||||||
|
final updatedState = {...state.storagePaths}..removeWhere((k, v) => k == storagePath.id);
|
||||||
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
|
return storagePath.id!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StoragePath?> findStoragePath(int id) async {
|
||||||
|
final storagePath = await _api.getStoragePath(id);
|
||||||
|
if (storagePath != null) {
|
||||||
|
final updatedState = {...state.storagePaths}..[id] = storagePath;
|
||||||
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
|
return storagePath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Iterable<StoragePath>> findAllStoragePaths([Iterable<int>? ids]) async {
|
||||||
|
final storagePaths = await _api.getStoragePaths(ids);
|
||||||
|
final updatedState = {...state.storagePaths}
|
||||||
|
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
|
||||||
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
|
return storagePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
|
||||||
|
final updated = await _api.updateStoragePath(storagePath);
|
||||||
|
final updatedState = {...state.storagePaths}..update(updated.id!, (_) => updated);
|
||||||
|
emit(state.copyWith(storagePaths: updatedState));
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_subscribers.forEach((key, subscription) {
|
||||||
|
subscription.cancel();
|
||||||
|
});
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
await super.clear();
|
||||||
|
emit(const LabelRepositoryState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
LabelRepositoryState? fromJson(Map<String, dynamic> json) {
|
||||||
|
return LabelRepositoryState.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic>? toJson(LabelRepositoryState state) {
|
||||||
|
return state.toJson();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
lib/core/repository/label_repository_state.dart
Normal file
18
lib/core/repository/label_repository_state.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
|
part 'label_repository_state.freezed.dart';
|
||||||
|
part 'label_repository_state.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class LabelRepositoryState with _$LabelRepositoryState {
|
||||||
|
const factory LabelRepositoryState({
|
||||||
|
@Default({}) Map<int, Correspondent> correspondents,
|
||||||
|
@Default({}) Map<int, DocumentType> documentTypes,
|
||||||
|
@Default({}) Map<int, Tag> tags,
|
||||||
|
@Default({}) Map<int, StoragePath> storagePaths,
|
||||||
|
}) = _LabelRepositoryState;
|
||||||
|
|
||||||
|
factory LabelRepositoryState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LabelRepositoryStateFromJson(json);
|
||||||
|
}
|
||||||
258
lib/core/repository/label_repository_state.freezed.dart
Normal file
258
lib/core/repository/label_repository_state.freezed.dart
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'label_repository_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
LabelRepositoryState _$LabelRepositoryStateFromJson(Map<String, dynamic> json) {
|
||||||
|
return _LabelRepositoryState.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$LabelRepositoryState {
|
||||||
|
Map<int, Correspondent> get correspondents =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, DocumentType> get documentTypes =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, Tag> get tags => throw _privateConstructorUsedError;
|
||||||
|
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$LabelRepositoryStateCopyWith<LabelRepositoryState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $LabelRepositoryStateCopyWith<$Res> {
|
||||||
|
factory $LabelRepositoryStateCopyWith(LabelRepositoryState value,
|
||||||
|
$Res Function(LabelRepositoryState) then) =
|
||||||
|
_$LabelRepositoryStateCopyWithImpl<$Res, LabelRepositoryState>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, Tag> tags,
|
||||||
|
Map<int, StoragePath> storagePaths});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$LabelRepositoryStateCopyWithImpl<$Res,
|
||||||
|
$Val extends LabelRepositoryState>
|
||||||
|
implements $LabelRepositoryStateCopyWith<$Res> {
|
||||||
|
_$LabelRepositoryStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? tags = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value.correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value.documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value.tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value.storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$_LabelRepositoryStateCopyWith<$Res>
|
||||||
|
implements $LabelRepositoryStateCopyWith<$Res> {
|
||||||
|
factory _$$_LabelRepositoryStateCopyWith(_$_LabelRepositoryState value,
|
||||||
|
$Res Function(_$_LabelRepositoryState) then) =
|
||||||
|
__$$_LabelRepositoryStateCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, Tag> tags,
|
||||||
|
Map<int, StoragePath> storagePaths});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$_LabelRepositoryStateCopyWithImpl<$Res>
|
||||||
|
extends _$LabelRepositoryStateCopyWithImpl<$Res, _$_LabelRepositoryState>
|
||||||
|
implements _$$_LabelRepositoryStateCopyWith<$Res> {
|
||||||
|
__$$_LabelRepositoryStateCopyWithImpl(_$_LabelRepositoryState _value,
|
||||||
|
$Res Function(_$_LabelRepositoryState) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? tags = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$_LabelRepositoryState(
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value._correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value._documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value._tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value._storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$_LabelRepositoryState implements _LabelRepositoryState {
|
||||||
|
const _$_LabelRepositoryState(
|
||||||
|
{final Map<int, Correspondent> correspondents = const {},
|
||||||
|
final Map<int, DocumentType> documentTypes = const {},
|
||||||
|
final Map<int, Tag> tags = const {},
|
||||||
|
final Map<int, StoragePath> storagePaths = const {}})
|
||||||
|
: _correspondents = correspondents,
|
||||||
|
_documentTypes = documentTypes,
|
||||||
|
_tags = tags,
|
||||||
|
_storagePaths = storagePaths;
|
||||||
|
|
||||||
|
factory _$_LabelRepositoryState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$_LabelRepositoryStateFromJson(json);
|
||||||
|
|
||||||
|
final Map<int, Correspondent> _correspondents;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, Correspondent> get correspondents {
|
||||||
|
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_correspondents);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, DocumentType> _documentTypes;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, DocumentType> get documentTypes {
|
||||||
|
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_documentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, Tag> _tags;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, Tag> get tags {
|
||||||
|
if (_tags is EqualUnmodifiableMapView) return _tags;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, StoragePath> _storagePaths;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, StoragePath> get storagePaths {
|
||||||
|
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_storagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'LabelRepositoryState(correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$_LabelRepositoryState &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._correspondents, _correspondents) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._documentTypes, _documentTypes) &&
|
||||||
|
const DeepCollectionEquality().equals(other._tags, _tags) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._storagePaths, _storagePaths));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
const DeepCollectionEquality().hash(_correspondents),
|
||||||
|
const DeepCollectionEquality().hash(_documentTypes),
|
||||||
|
const DeepCollectionEquality().hash(_tags),
|
||||||
|
const DeepCollectionEquality().hash(_storagePaths));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$_LabelRepositoryStateCopyWith<_$_LabelRepositoryState> get copyWith =>
|
||||||
|
__$$_LabelRepositoryStateCopyWithImpl<_$_LabelRepositoryState>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$_LabelRepositoryStateToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _LabelRepositoryState implements LabelRepositoryState {
|
||||||
|
const factory _LabelRepositoryState(
|
||||||
|
{final Map<int, Correspondent> correspondents,
|
||||||
|
final Map<int, DocumentType> documentTypes,
|
||||||
|
final Map<int, Tag> tags,
|
||||||
|
final Map<int, StoragePath> storagePaths}) = _$_LabelRepositoryState;
|
||||||
|
|
||||||
|
factory _LabelRepositoryState.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$_LabelRepositoryState.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<int, Correspondent> get correspondents;
|
||||||
|
@override
|
||||||
|
Map<int, DocumentType> get documentTypes;
|
||||||
|
@override
|
||||||
|
Map<int, Tag> get tags;
|
||||||
|
@override
|
||||||
|
Map<int, StoragePath> get storagePaths;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$_LabelRepositoryStateCopyWith<_$_LabelRepositoryState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
|
||||||
|
|
||||||
class LabelRepositoriesProvider extends StatelessWidget {
|
|
||||||
final Widget child;
|
|
||||||
const LabelRepositoriesProvider({super.key, required this.child});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MultiRepositoryProvider(
|
|
||||||
providers: [
|
|
||||||
RepositoryProvider(
|
|
||||||
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
|
||||||
),
|
|
||||||
RepositoryProvider(
|
|
||||||
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
|
||||||
),
|
|
||||||
RepositoryProvider(
|
|
||||||
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
|
||||||
),
|
|
||||||
RepositoryProvider(
|
|
||||||
create: (context) => context.read<LabelRepository<Tag>>(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,84 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'dart:async';
|
||||||
import 'package:paperless_mobile/core/repository/base_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
|
|
||||||
abstract class SavedViewRepository extends BaseRepository<SavedView> {
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
SavedViewRepository(super.initialState);
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/saved_view_repository_state.dart';
|
||||||
|
|
||||||
|
class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
|
||||||
|
final PaperlessSavedViewsApi _api;
|
||||||
|
final Map<Object, StreamSubscription> _subscribers = {};
|
||||||
|
|
||||||
|
void subscribe(
|
||||||
|
Object source,
|
||||||
|
void Function(Map<int, SavedView>) onChanged,
|
||||||
|
) {
|
||||||
|
_subscribers.putIfAbsent(source, () {
|
||||||
|
onChanged(state.savedViews);
|
||||||
|
return stream.listen((event) => onChanged(event.savedViews));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void unsubscribe(Object source) async {
|
||||||
|
await _subscribers[source]?.cancel();
|
||||||
|
_subscribers.remove(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
SavedViewRepository(this._api) : super(const SavedViewRepositoryState());
|
||||||
|
|
||||||
|
Future<SavedView> create(SavedView object) async {
|
||||||
|
final created = await _api.save(object);
|
||||||
|
final updatedState = {...state.savedViews}..putIfAbsent(created.id!, () => created);
|
||||||
|
emit(state.copyWith(savedViews: updatedState));
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> delete(SavedView view) async {
|
||||||
|
await _api.delete(view);
|
||||||
|
final updatedState = {...state.savedViews}..remove(view.id);
|
||||||
|
emit(state.copyWith(savedViews: updatedState));
|
||||||
|
return view.id!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SavedView?> find(int id) async {
|
||||||
|
final found = await _api.find(id);
|
||||||
|
if (found != null) {
|
||||||
|
final updatedState = {...state.savedViews}..update(id, (_) => found, ifAbsent: () => found);
|
||||||
|
emit(state.copyWith(savedViews: updatedState));
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
|
||||||
|
final found = await _api.findAll(ids);
|
||||||
|
final updatedState = {
|
||||||
|
...state.savedViews,
|
||||||
|
...{for (final view in found) view.id!: view},
|
||||||
|
};
|
||||||
|
emit(state.copyWith(savedViews: updatedState));
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_subscribers.forEach((key, subscription) {
|
||||||
|
subscription.cancel();
|
||||||
|
});
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
await super.clear();
|
||||||
|
emit(const SavedViewRepositoryState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SavedViewRepositoryState? fromJson(Map<String, dynamic> json) {
|
||||||
|
return SavedViewRepositoryState.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic>? toJson(SavedViewRepositoryState state) {
|
||||||
|
return state.toJson();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
lib/core/repository/saved_view_repository_state.dart
Normal file
15
lib/core/repository/saved_view_repository_state.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
|
part 'saved_view_repository_state.freezed.dart';
|
||||||
|
part 'saved_view_repository_state.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SavedViewRepositoryState with _$SavedViewRepositoryState {
|
||||||
|
const factory SavedViewRepositoryState({
|
||||||
|
@Default({}) Map<int, SavedView> savedViews,
|
||||||
|
}) = _SavedViewRepositoryState;
|
||||||
|
|
||||||
|
factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SavedViewRepositoryStateFromJson(json);
|
||||||
|
}
|
||||||
167
lib/core/repository/saved_view_repository_state.freezed.dart
Normal file
167
lib/core/repository/saved_view_repository_state.freezed.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'saved_view_repository_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
SavedViewRepositoryState _$SavedViewRepositoryStateFromJson(
|
||||||
|
Map<String, dynamic> json) {
|
||||||
|
return _SavedViewRepositoryState.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SavedViewRepositoryState {
|
||||||
|
Map<int, SavedView> get savedViews => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$SavedViewRepositoryStateCopyWith<SavedViewRepositoryState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SavedViewRepositoryStateCopyWith<$Res> {
|
||||||
|
factory $SavedViewRepositoryStateCopyWith(SavedViewRepositoryState value,
|
||||||
|
$Res Function(SavedViewRepositoryState) then) =
|
||||||
|
_$SavedViewRepositoryStateCopyWithImpl<$Res, SavedViewRepositoryState>;
|
||||||
|
@useResult
|
||||||
|
$Res call({Map<int, SavedView> savedViews});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SavedViewRepositoryStateCopyWithImpl<$Res,
|
||||||
|
$Val extends SavedViewRepositoryState>
|
||||||
|
implements $SavedViewRepositoryStateCopyWith<$Res> {
|
||||||
|
_$SavedViewRepositoryStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? savedViews = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
savedViews: null == savedViews
|
||||||
|
? _value.savedViews
|
||||||
|
: savedViews // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, SavedView>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$_SavedViewRepositoryStateCopyWith<$Res>
|
||||||
|
implements $SavedViewRepositoryStateCopyWith<$Res> {
|
||||||
|
factory _$$_SavedViewRepositoryStateCopyWith(
|
||||||
|
_$_SavedViewRepositoryState value,
|
||||||
|
$Res Function(_$_SavedViewRepositoryState) then) =
|
||||||
|
__$$_SavedViewRepositoryStateCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({Map<int, SavedView> savedViews});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$_SavedViewRepositoryStateCopyWithImpl<$Res>
|
||||||
|
extends _$SavedViewRepositoryStateCopyWithImpl<$Res,
|
||||||
|
_$_SavedViewRepositoryState>
|
||||||
|
implements _$$_SavedViewRepositoryStateCopyWith<$Res> {
|
||||||
|
__$$_SavedViewRepositoryStateCopyWithImpl(_$_SavedViewRepositoryState _value,
|
||||||
|
$Res Function(_$_SavedViewRepositoryState) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? savedViews = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$_SavedViewRepositoryState(
|
||||||
|
savedViews: null == savedViews
|
||||||
|
? _value._savedViews
|
||||||
|
: savedViews // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, SavedView>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$_SavedViewRepositoryState implements _SavedViewRepositoryState {
|
||||||
|
const _$_SavedViewRepositoryState(
|
||||||
|
{final Map<int, SavedView> savedViews = const {}})
|
||||||
|
: _savedViews = savedViews;
|
||||||
|
|
||||||
|
factory _$_SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$_SavedViewRepositoryStateFromJson(json);
|
||||||
|
|
||||||
|
final Map<int, SavedView> _savedViews;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, SavedView> get savedViews {
|
||||||
|
if (_savedViews is EqualUnmodifiableMapView) return _savedViews;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_savedViews);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SavedViewRepositoryState(savedViews: $savedViews)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$_SavedViewRepositoryState &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._savedViews, _savedViews));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType, const DeepCollectionEquality().hash(_savedViews));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$_SavedViewRepositoryStateCopyWith<_$_SavedViewRepositoryState>
|
||||||
|
get copyWith => __$$_SavedViewRepositoryStateCopyWithImpl<
|
||||||
|
_$_SavedViewRepositoryState>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$_SavedViewRepositoryStateToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SavedViewRepositoryState implements SavedViewRepositoryState {
|
||||||
|
const factory _SavedViewRepositoryState(
|
||||||
|
{final Map<int, SavedView> savedViews}) = _$_SavedViewRepositoryState;
|
||||||
|
|
||||||
|
factory _SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$_SavedViewRepositoryState.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<int, SavedView> get savedViews;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$_SavedViewRepositoryStateCopyWith<_$_SavedViewRepositoryState>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
|
|
||||||
part 'correspondent_repository_state.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class CorrespondentRepositoryState
|
|
||||||
extends IndexedRepositoryState<Correspondent> {
|
|
||||||
const CorrespondentRepositoryState({
|
|
||||||
super.values = const {},
|
|
||||||
super.hasLoaded,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
CorrespondentRepositoryState copyWith({
|
|
||||||
Map<int, Correspondent>? values,
|
|
||||||
bool? hasLoaded,
|
|
||||||
}) {
|
|
||||||
return CorrespondentRepositoryState(
|
|
||||||
values: values ?? this.values,
|
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory CorrespondentRepositoryState.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$CorrespondentRepositoryStateFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$CorrespondentRepositoryStateToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'document_type_repository_state.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class DocumentTypeRepositoryState extends IndexedRepositoryState<DocumentType> {
|
|
||||||
const DocumentTypeRepositoryState({
|
|
||||||
super.values = const {},
|
|
||||||
super.hasLoaded,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
DocumentTypeRepositoryState copyWith({
|
|
||||||
Map<int, DocumentType>? values,
|
|
||||||
bool? hasLoaded,
|
|
||||||
}) {
|
|
||||||
return DocumentTypeRepositoryState(
|
|
||||||
values: values ?? this.values,
|
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory DocumentTypeRepositoryState.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DocumentTypeRepositoryStateFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$DocumentTypeRepositoryStateToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'saved_view_repository_state.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class SavedViewRepositoryState extends IndexedRepositoryState<SavedView> {
|
|
||||||
const SavedViewRepositoryState({
|
|
||||||
super.values = const {},
|
|
||||||
super.hasLoaded = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
SavedViewRepositoryState copyWith({
|
|
||||||
Map<int, SavedView>? values,
|
|
||||||
bool? hasLoaded,
|
|
||||||
}) {
|
|
||||||
return SavedViewRepositoryState(
|
|
||||||
values: values ?? this.values,
|
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SavedViewRepositoryStateFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SavedViewRepositoryStateToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'storage_path_repository_state.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class StoragePathRepositoryState extends IndexedRepositoryState<StoragePath> {
|
|
||||||
const StoragePathRepositoryState({
|
|
||||||
super.values = const {},
|
|
||||||
super.hasLoaded = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
StoragePathRepositoryState copyWith({
|
|
||||||
Map<int, StoragePath>? values,
|
|
||||||
bool? hasLoaded,
|
|
||||||
}) {
|
|
||||||
return StoragePathRepositoryState(
|
|
||||||
values: values ?? this.values,
|
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory StoragePathRepositoryState.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$StoragePathRepositoryStateFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$StoragePathRepositoryStateToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
|
||||||
|
|
||||||
part 'tag_repository_state.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class TagRepositoryState extends IndexedRepositoryState<Tag> {
|
|
||||||
const TagRepositoryState({
|
|
||||||
super.values = const {},
|
|
||||||
super.hasLoaded = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
TagRepositoryState copyWith({
|
|
||||||
Map<int, Tag>? values,
|
|
||||||
bool? hasLoaded,
|
|
||||||
}) {
|
|
||||||
return TagRepositoryState(
|
|
||||||
values: values ?? this.values,
|
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory TagRepositoryState.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$TagRepositoryStateFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$TagRepositoryStateToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
abstract class IndexedRepositoryState<T> {
|
|
||||||
final Map<int, T>? values;
|
|
||||||
final bool hasLoaded;
|
|
||||||
|
|
||||||
const IndexedRepositoryState({
|
|
||||||
required this.values,
|
|
||||||
this.hasLoaded = false,
|
|
||||||
}) : assert(!(values == null) || !hasLoaded);
|
|
||||||
|
|
||||||
IndexedRepositoryState.loaded(this.values) : hasLoaded = true;
|
|
||||||
|
|
||||||
IndexedRepositoryState<T> copyWith({
|
|
||||||
Map<int, T>? values,
|
|
||||||
bool? hasLoaded,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -8,14 +7,18 @@ import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_int
|
|||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||||
|
|
||||||
|
/// Manages the security context, authentication and base request URL for
|
||||||
|
/// an underlying [Dio] client which is injected into all services
|
||||||
|
/// requiring authenticated access to the Paperless HTTP API.
|
||||||
class SessionManager {
|
class SessionManager {
|
||||||
final Dio client;
|
final Dio _client;
|
||||||
final List<Interceptor> interceptors;
|
PaperlessServerInformationModel _serverInformation;
|
||||||
PaperlessServerInformationModel serverInformation;
|
|
||||||
|
|
||||||
SessionManager([this.interceptors = const []])
|
Dio get client => _client;
|
||||||
: client = _initDio(interceptors),
|
|
||||||
serverInformation = PaperlessServerInformationModel();
|
SessionManager([List<Interceptor> interceptors = const []])
|
||||||
|
: _client = _initDio(interceptors),
|
||||||
|
_serverInformation = PaperlessServerInformationModel();
|
||||||
|
|
||||||
static Dio _initDio(List<Interceptor> interceptors) {
|
static Dio _initDio(List<Interceptor> interceptors) {
|
||||||
//en- and decoded by utf8 by default
|
//en- and decoded by utf8 by default
|
||||||
@@ -63,8 +66,7 @@ class SessionManager {
|
|||||||
);
|
);
|
||||||
final adapter = IOHttpClientAdapter()
|
final adapter = IOHttpClientAdapter()
|
||||||
..onHttpClientCreate = (client) => HttpClient(context: context)
|
..onHttpClientCreate = (client) => HttpClient(context: context)
|
||||||
..badCertificateCallback =
|
..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
||||||
(X509Certificate cert, String host, int port) => true;
|
|
||||||
|
|
||||||
client.httpClientAdapter = adapter;
|
client.httpClientAdapter = adapter;
|
||||||
}
|
}
|
||||||
@@ -80,7 +82,7 @@ class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (serverInformation != null) {
|
if (serverInformation != null) {
|
||||||
this.serverInformation = serverInformation;
|
_serverInformation = serverInformation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +90,6 @@ class SessionManager {
|
|||||||
client.httpClientAdapter = IOHttpClientAdapter();
|
client.httpClientAdapter = IOHttpClientAdapter();
|
||||||
client.options.baseUrl = '';
|
client.options.baseUrl = '';
|
||||||
client.options.headers.remove(HttpHeaders.authorizationHeader);
|
client.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||||
serverInformation = PaperlessServerInformationModel();
|
_serverInformation = PaperlessServerInformationModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isConnectedToInternet() async {
|
Future<bool> isConnectedToInternet() async {
|
||||||
return _hasActiveInternetConnection(
|
return _hasActiveInternetConnection(await (Connectivity().checkConnectivity()));
|
||||||
await (Connectivity().checkConnectivity()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -72,11 +71,10 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
return ReachabilityStatus.unknown;
|
return ReachabilityStatus.unknown;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
SessionManager manager =
|
SessionManager manager = SessionManager([ServerReachabilityErrorInterceptor()])
|
||||||
SessionManager([ServerReachabilityErrorInterceptor()])
|
..updateSettings(clientCertificate: clientCertificate)
|
||||||
..updateSettings(clientCertificate: clientCertificate)
|
..client.options.connectTimeout = const Duration(seconds: 5)
|
||||||
..client.options.connectTimeout = const Duration(seconds: 5)
|
..client.options.receiveTimeout = const Duration(seconds: 5);
|
||||||
..client.options.receiveTimeout = const Duration(seconds: 5);
|
|
||||||
|
|
||||||
final response = await manager.client.get('$serverAddress/api/');
|
final response = await manager.client.get('$serverAddress/api/');
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
@@ -84,8 +82,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
}
|
}
|
||||||
return ReachabilityStatus.notReachable;
|
return ReachabilityStatus.notReachable;
|
||||||
} on DioError catch (error) {
|
} on DioError catch (error) {
|
||||||
if (error.type == DioErrorType.unknown &&
|
if (error.type == DioErrorType.unknown && error.error is ReachabilityStatus) {
|
||||||
error.error is ReachabilityStatus) {
|
|
||||||
return error.error as ReachabilityStatus;
|
return error.error as ReachabilityStatus;
|
||||||
}
|
}
|
||||||
} on TlsException catch (error) {
|
} on TlsException catch (error) {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
|
|||||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
abstract class StatusService {
|
abstract class StatusService {
|
||||||
Future<void> startListeningBeforeDocumentUpload(String httpUrl,
|
Future<void> startListeningBeforeDocumentUpload(
|
||||||
AuthenticationInformation credentials, String documentFileName);
|
String httpUrl, UserCredentials credentials, String documentFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketStatusService implements StatusService {
|
class WebSocketStatusService implements StatusService {
|
||||||
@@ -25,7 +26,7 @@ class WebSocketStatusService implements StatusService {
|
|||||||
@override
|
@override
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
Future<void> startListeningBeforeDocumentUpload(
|
||||||
String httpUrl,
|
String httpUrl,
|
||||||
AuthenticationInformation credentials,
|
UserCredentials credentials,
|
||||||
String documentFileName,
|
String documentFileName,
|
||||||
) async {
|
) async {
|
||||||
// socket = await WebSocket.connect(
|
// socket = await WebSocket.connect(
|
||||||
@@ -57,7 +58,7 @@ class LongPollingStatusService implements StatusService {
|
|||||||
@override
|
@override
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
Future<void> startListeningBeforeDocumentUpload(
|
||||||
String httpUrl,
|
String httpUrl,
|
||||||
AuthenticationInformation credentials,
|
UserCredentials credentials,
|
||||||
String documentFileName,
|
String documentFileName,
|
||||||
) async {
|
) async {
|
||||||
// final today = DateTime.now();
|
// final today = DateTime.now();
|
||||||
|
|||||||
56
lib/core/widgets/dialog_utils/dialog_confirm_button.dart
Normal file
56
lib/core/widgets/dialog_utils/dialog_confirm_button.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
|
import 'package:flutter/src/widgets/placeholder.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
enum DialogConfirmButtonStyle {
|
||||||
|
normal,
|
||||||
|
danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DialogConfirmButton<T> extends StatelessWidget {
|
||||||
|
final DialogConfirmButtonStyle style;
|
||||||
|
final String? label;
|
||||||
|
final T? returnValue;
|
||||||
|
const DialogConfirmButton({
|
||||||
|
super.key,
|
||||||
|
this.style = DialogConfirmButtonStyle.normal,
|
||||||
|
this.label,
|
||||||
|
this.returnValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final _normalStyle = ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
foregroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final _dangerStyle = ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
foregroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
late final ButtonStyle _style;
|
||||||
|
switch (style) {
|
||||||
|
case DialogConfirmButtonStyle.normal:
|
||||||
|
_style = _normalStyle;
|
||||||
|
break;
|
||||||
|
case DialogConfirmButtonStyle.danger:
|
||||||
|
_style = _dangerStyle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ElevatedButton(
|
||||||
|
child: Text(label ?? S.of(context)!.confirm),
|
||||||
|
style: _style,
|
||||||
|
onPressed: () => Navigator.of(context).pop(returnValue ?? true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
import 'package:paperless_mobile/core/model/github_error_report.model.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/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
class ErrorReportPage extends StatefulWidget {
|
class ErrorReportPage extends StatefulWidget {
|
||||||
@@ -136,10 +137,7 @@ Note: If you have the GitHub Android app installed, the descriptions will not be
|
|||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
child: const Text('Cancel'),
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -56,7 +56,9 @@ class _FormBuilderRelativeDateRangePickerState
|
|||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
],
|
],
|
||||||
validator: FormBuilderValidators.numeric(),
|
// validator: (value) { //TODO: Check if this is required
|
||||||
|
// do numeric validation
|
||||||
|
// },
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final parsed = int.tryParse(value);
|
final parsed = int.tryParse(value);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.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';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
extension on Color {
|
extension on Color {
|
||||||
@@ -136,11 +138,12 @@ class FormBuilderColorPickerField extends FormBuilderField<Color> {
|
|||||||
: LayoutBuilder(
|
: LayoutBuilder(
|
||||||
key: ObjectKey(state.value),
|
key: ObjectKey(state.value),
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Icon(
|
return Padding(
|
||||||
Icons.circle,
|
padding: const EdgeInsets.all(8.0),
|
||||||
key: ObjectKey(state.value),
|
child: CircleAvatar(
|
||||||
size: constraints.minHeight,
|
key: ObjectKey(state.value),
|
||||||
color: state.value,
|
backgroundColor: state.value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -218,17 +221,11 @@ class FormBuilderColorPickerFieldState
|
|||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
// title: null, //const Text('Pick a color!'),
|
// title: null, //const Text('Pick a color!'),
|
||||||
content: SingleChildScrollView(
|
content: _buildColorPicker(),
|
||||||
child: _buildColorPicker(),
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.pop(context, false),
|
DialogConfirmButton(
|
||||||
child: Text(materialLocalizations.cancel),
|
label: S.of(context)!.ok,
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: Text(materialLocalizations.ok),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
155
lib/core/widgets/form_fields/fullscreen_selection_form.dart
Normal file
155
lib/core/widgets/form_fields/fullscreen_selection_form.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class FullscreenSelectionForm extends StatefulWidget {
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
final String hintText;
|
||||||
|
final Widget leadingIcon;
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
final VoidCallback? onTextFieldCleared;
|
||||||
|
final List<Widget> trailingActions;
|
||||||
|
final Widget Function(BuildContext context, int index) selectionBuilder;
|
||||||
|
final int selectionCount;
|
||||||
|
final void Function(String value)? onKeyboardSubmit;
|
||||||
|
final Widget? floatingActionButton;
|
||||||
|
|
||||||
|
const FullscreenSelectionForm({
|
||||||
|
super.key,
|
||||||
|
this.focusNode,
|
||||||
|
this.controller,
|
||||||
|
required this.hintText,
|
||||||
|
required this.leadingIcon,
|
||||||
|
this.autofocus = true,
|
||||||
|
this.onTextFieldCleared,
|
||||||
|
this.trailingActions = const [],
|
||||||
|
required this.selectionBuilder,
|
||||||
|
required this.selectionCount,
|
||||||
|
this.onKeyboardSubmit,
|
||||||
|
this.floatingActionButton,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FullscreenSelectionForm> createState() =>
|
||||||
|
_FullscreenSelectionFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FullscreenSelectionFormState extends State<FullscreenSelectionForm> {
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
bool _showClearIcon = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
_controller = (widget.controller ?? TextEditingController())
|
||||||
|
..addListener(() {
|
||||||
|
setState(() {
|
||||||
|
_showClearIcon = _controller.text.isNotEmpty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (widget.autofocus) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
//Delay keyboard popup to ensure open animation is finished before.
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
() => _focusNode.requestFocus(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
floatingActionButton: widget.floatingActionButton,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
toolbarHeight: 72,
|
||||||
|
leading: BackButton(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
title: TextFormField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
controller: _controller,
|
||||||
|
onFieldSubmitted: (value) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
widget.onKeyboardSubmit?.call(value);
|
||||||
|
},
|
||||||
|
autofocus: true,
|
||||||
|
style: theme.textTheme.bodyLarge?.apply(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
hintStyle: theme.textTheme.bodyLarge?.apply(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
icon: widget.leadingIcon,
|
||||||
|
hintText: widget.hintText,
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (_showClearIcon)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_controller.clear();
|
||||||
|
widget.onTextFieldCleared?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
...widget.trailingActions,
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Divider(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Builder(builder: (context) {
|
||||||
|
if (widget.selectionCount == 0) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Text(S.of(context)!.noItemsFound).padded(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: widget.selectionCount,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final highlight =
|
||||||
|
AutocompleteHighlightedOption.of(context) == index;
|
||||||
|
if (highlight) {
|
||||||
|
SchedulerBinding.instance
|
||||||
|
.addPostFrameCallback((Duration timeStamp) {
|
||||||
|
Scrollable.ensureVisible(
|
||||||
|
context,
|
||||||
|
alignment: 0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return widget.selectionBuilder(context, index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
291
lib/core/widgets/material/chips_input.dart
Normal file
291
lib/core/widgets/material/chips_input.dart
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
// 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.duration = const Duration(milliseconds: 500),
|
||||||
|
this.resumed = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Duration duration;
|
||||||
|
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();
|
||||||
|
_timer = Timer.periodic(widget.duration, _onTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1885
lib/core/widgets/material/search/search_anchor.dart
Normal file
1885
lib/core/widgets/material/search/search_anchor.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class OfflineBanner extends StatelessWidget with PreferredSizeWidget {
|
class OfflineBanner extends StatelessWidget implements PreferredSizeWidget {
|
||||||
const OfflineBanner({super.key});
|
const OfflineBanner({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -36,3 +36,9 @@ extension DateHelpers on DateTime {
|
|||||||
yesterday.year == year;
|
yesterday.year == year;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StringNormalizer on String {
|
||||||
|
String normalized() {
|
||||||
|
return trim().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
|
||||||
|
|
||||||
extension AddressableHydratedStorage on Storage {
|
|
||||||
ApplicationSettingsState get settings {
|
|
||||||
return ApplicationSettingsState.fromJson(read('ApplicationSettingsCubit'));
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationState get authentication {
|
|
||||||
return AuthenticationState.fromJson(read('AuthenticationCubit'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.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_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/server_information_cubit.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/settings/cubit/application_settings_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -42,8 +42,7 @@ class AppDrawer extends StatelessWidget {
|
|||||||
leading: const Icon(Icons.bug_report_outlined),
|
leading: const Icon(Icons.bug_report_outlined),
|
||||||
title: Text(S.of(context)!.reportABug),
|
title: Text(S.of(context)!.reportABug),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString('https://github.com/astubenbord/paperless-mobile/issues/new');
|
||||||
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -69,8 +68,8 @@ class AppDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BlocProvider.value(
|
builder: (_) => BlocProvider.value(
|
||||||
value: context.read<ApplicationSettingsCubit>(),
|
value: context.read<ServerInformationCubit>(),
|
||||||
child: const SettingsPage(),
|
child: const SettingsPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ class ApplicationIntroSlideshow extends StatefulWidget {
|
|||||||
const ApplicationIntroSlideshow({super.key});
|
const ApplicationIntroSlideshow({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ApplicationIntroSlideshow> createState() =>
|
State<ApplicationIntroSlideshow> createState() => _ApplicationIntroSlideshowState();
|
||||||
_ApplicationIntroSlideshowState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: INTL ALL
|
//TODO: INTL ALL
|
||||||
@@ -28,7 +27,9 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
|
|||||||
showDoneButton: true,
|
showDoneButton: true,
|
||||||
next: Text(S.of(context)!.next),
|
next: Text(S.of(context)!.next),
|
||||||
done: Text(S.of(context)!.done),
|
done: Text(S.of(context)!.done),
|
||||||
onDone: () => Navigator.pop(context),
|
onDone: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
dotsDecorator: DotsDecorator(
|
dotsDecorator: DotsDecorator(
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'document_bulk_action_state.dart';
|
||||||
|
part 'document_bulk_action_cubit.freezed.dart';
|
||||||
|
|
||||||
|
class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
|
||||||
|
final PaperlessDocumentsApi _documentsApi;
|
||||||
|
final LabelRepository _labelRepository;
|
||||||
|
final DocumentChangedNotifier _notifier;
|
||||||
|
|
||||||
|
DocumentBulkActionCubit(
|
||||||
|
this._documentsApi,
|
||||||
|
this._labelRepository,
|
||||||
|
this._notifier, {
|
||||||
|
required List<DocumentModel> selection,
|
||||||
|
}) : super(
|
||||||
|
DocumentBulkActionState(
|
||||||
|
selection: selection,
|
||||||
|
correspondents: _labelRepository.state.correspondents,
|
||||||
|
documentTypes: _labelRepository.state.documentTypes,
|
||||||
|
storagePaths: _labelRepository.state.storagePaths,
|
||||||
|
tags: _labelRepository.state.tags,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
_notifier.addListener(
|
||||||
|
this,
|
||||||
|
onDeleted: (document) {
|
||||||
|
// Remove items from internal selection after the document was deleted.
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selection: state.selection
|
||||||
|
.whereNot((element) => element.id == document.id)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
_labelRepository.addListener(
|
||||||
|
this,
|
||||||
|
onChanged: (labels) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
correspondents: labels.correspondents,
|
||||||
|
documentTypes: labels.documentTypes,
|
||||||
|
storagePaths: labels.storagePaths,
|
||||||
|
tags: labels.tags,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> bulkDelete() async {
|
||||||
|
final deletedDocumentIds = await _documentsApi.bulkAction(
|
||||||
|
BulkDeleteAction(state.selection.map((e) => e.id).toList()),
|
||||||
|
);
|
||||||
|
final deletedDocuments = state.selection
|
||||||
|
.where((element) => deletedDocumentIds.contains(element.id));
|
||||||
|
for (final doc in deletedDocuments) {
|
||||||
|
_notifier.notifyDeleted(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> bulkModifyCorrespondent(int? correspondentId) async {
|
||||||
|
final modifiedDocumentIds = await _documentsApi.bulkAction(
|
||||||
|
BulkModifyLabelAction.correspondent(
|
||||||
|
state.selectedIds,
|
||||||
|
labelId: correspondentId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final updatedDocuments = state.selection
|
||||||
|
.where((element) => modifiedDocumentIds.contains(element.id))
|
||||||
|
.map((doc) => doc.copyWith(correspondent: () => correspondentId));
|
||||||
|
for (final doc in updatedDocuments) {
|
||||||
|
_notifier.notifyUpdated(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> bulkModifyDocumentType(int? documentTypeId) async {
|
||||||
|
final modifiedDocumentIds = await _documentsApi.bulkAction(
|
||||||
|
BulkModifyLabelAction.documentType(
|
||||||
|
state.selectedIds,
|
||||||
|
labelId: documentTypeId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final updatedDocuments = state.selection
|
||||||
|
.where((element) => modifiedDocumentIds.contains(element.id))
|
||||||
|
.map((doc) => doc.copyWith(documentType: () => documentTypeId));
|
||||||
|
for (final doc in updatedDocuments) {
|
||||||
|
_notifier.notifyUpdated(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> bulkModifyStoragePath(int? storagePathId) async {
|
||||||
|
final modifiedDocumentIds = await _documentsApi.bulkAction(
|
||||||
|
BulkModifyLabelAction.storagePath(
|
||||||
|
state.selectedIds,
|
||||||
|
labelId: storagePathId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final updatedDocuments = state.selection
|
||||||
|
.where((element) => modifiedDocumentIds.contains(element.id))
|
||||||
|
.map((doc) => doc.copyWith(storagePath: () => storagePathId));
|
||||||
|
for (final doc in updatedDocuments) {
|
||||||
|
_notifier.notifyUpdated(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> bulkModifyTags({
|
||||||
|
Iterable<int> addTagIds = const [],
|
||||||
|
Iterable<int> removeTagIds = const [],
|
||||||
|
}) async {
|
||||||
|
final modifiedDocumentIds = await _documentsApi.bulkAction(
|
||||||
|
BulkModifyTagsAction(
|
||||||
|
state.selectedIds,
|
||||||
|
addTags: addTagIds,
|
||||||
|
removeTags: removeTagIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final updatedDocuments = state.selection
|
||||||
|
.where((element) => modifiedDocumentIds.contains(element.id))
|
||||||
|
.map((doc) => doc.copyWith(tags: [
|
||||||
|
...doc.tags.toSet().difference(removeTagIds.toSet()),
|
||||||
|
...addTagIds
|
||||||
|
]));
|
||||||
|
for (final doc in updatedDocuments) {
|
||||||
|
_notifier.notifyUpdated(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_notifier.removeListener(this);
|
||||||
|
_labelRepository.removeListener(this);
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'document_bulk_action_cubit.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$DocumentBulkActionState {
|
||||||
|
List<DocumentModel> get selection => throw _privateConstructorUsedError;
|
||||||
|
Map<int, Correspondent> get correspondents =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, DocumentType> get documentTypes =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, Tag> get tags => throw _privateConstructorUsedError;
|
||||||
|
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$DocumentBulkActionStateCopyWith<DocumentBulkActionState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $DocumentBulkActionStateCopyWith<$Res> {
|
||||||
|
factory $DocumentBulkActionStateCopyWith(DocumentBulkActionState value,
|
||||||
|
$Res Function(DocumentBulkActionState) then) =
|
||||||
|
_$DocumentBulkActionStateCopyWithImpl<$Res, DocumentBulkActionState>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{List<DocumentModel> selection,
|
||||||
|
Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, Tag> tags,
|
||||||
|
Map<int, StoragePath> storagePaths});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$DocumentBulkActionStateCopyWithImpl<$Res,
|
||||||
|
$Val extends DocumentBulkActionState>
|
||||||
|
implements $DocumentBulkActionStateCopyWith<$Res> {
|
||||||
|
_$DocumentBulkActionStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? selection = null,
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? tags = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
selection: null == selection
|
||||||
|
? _value.selection
|
||||||
|
: selection // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<DocumentModel>,
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value.correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value.documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value.tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value.storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$_DocumentBulkActionStateCopyWith<$Res>
|
||||||
|
implements $DocumentBulkActionStateCopyWith<$Res> {
|
||||||
|
factory _$$_DocumentBulkActionStateCopyWith(_$_DocumentBulkActionState value,
|
||||||
|
$Res Function(_$_DocumentBulkActionState) then) =
|
||||||
|
__$$_DocumentBulkActionStateCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{List<DocumentModel> selection,
|
||||||
|
Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, Tag> tags,
|
||||||
|
Map<int, StoragePath> storagePaths});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$_DocumentBulkActionStateCopyWithImpl<$Res>
|
||||||
|
extends _$DocumentBulkActionStateCopyWithImpl<$Res,
|
||||||
|
_$_DocumentBulkActionState>
|
||||||
|
implements _$$_DocumentBulkActionStateCopyWith<$Res> {
|
||||||
|
__$$_DocumentBulkActionStateCopyWithImpl(_$_DocumentBulkActionState _value,
|
||||||
|
$Res Function(_$_DocumentBulkActionState) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? selection = null,
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? tags = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$_DocumentBulkActionState(
|
||||||
|
selection: null == selection
|
||||||
|
? _value._selection
|
||||||
|
: selection // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<DocumentModel>,
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value._correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value._documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value._tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value._storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_DocumentBulkActionState extends _DocumentBulkActionState {
|
||||||
|
const _$_DocumentBulkActionState(
|
||||||
|
{required final List<DocumentModel> selection,
|
||||||
|
required final Map<int, Correspondent> correspondents,
|
||||||
|
required final Map<int, DocumentType> documentTypes,
|
||||||
|
required final Map<int, Tag> tags,
|
||||||
|
required final Map<int, StoragePath> storagePaths})
|
||||||
|
: _selection = selection,
|
||||||
|
_correspondents = correspondents,
|
||||||
|
_documentTypes = documentTypes,
|
||||||
|
_tags = tags,
|
||||||
|
_storagePaths = storagePaths,
|
||||||
|
super._();
|
||||||
|
|
||||||
|
final List<DocumentModel> _selection;
|
||||||
|
@override
|
||||||
|
List<DocumentModel> get selection {
|
||||||
|
if (_selection is EqualUnmodifiableListView) return _selection;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, Correspondent> _correspondents;
|
||||||
|
@override
|
||||||
|
Map<int, Correspondent> get correspondents {
|
||||||
|
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_correspondents);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, DocumentType> _documentTypes;
|
||||||
|
@override
|
||||||
|
Map<int, DocumentType> get documentTypes {
|
||||||
|
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_documentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, Tag> _tags;
|
||||||
|
@override
|
||||||
|
Map<int, Tag> get tags {
|
||||||
|
if (_tags is EqualUnmodifiableMapView) return _tags;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, StoragePath> _storagePaths;
|
||||||
|
@override
|
||||||
|
Map<int, StoragePath> get storagePaths {
|
||||||
|
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_storagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DocumentBulkActionState(selection: $selection, correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$_DocumentBulkActionState &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._selection, _selection) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._correspondents, _correspondents) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._documentTypes, _documentTypes) &&
|
||||||
|
const DeepCollectionEquality().equals(other._tags, _tags) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._storagePaths, _storagePaths));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
const DeepCollectionEquality().hash(_selection),
|
||||||
|
const DeepCollectionEquality().hash(_correspondents),
|
||||||
|
const DeepCollectionEquality().hash(_documentTypes),
|
||||||
|
const DeepCollectionEquality().hash(_tags),
|
||||||
|
const DeepCollectionEquality().hash(_storagePaths));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$_DocumentBulkActionStateCopyWith<_$_DocumentBulkActionState>
|
||||||
|
get copyWith =>
|
||||||
|
__$$_DocumentBulkActionStateCopyWithImpl<_$_DocumentBulkActionState>(
|
||||||
|
this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _DocumentBulkActionState extends DocumentBulkActionState {
|
||||||
|
const factory _DocumentBulkActionState(
|
||||||
|
{required final List<DocumentModel> selection,
|
||||||
|
required final Map<int, Correspondent> correspondents,
|
||||||
|
required final Map<int, DocumentType> documentTypes,
|
||||||
|
required final Map<int, Tag> tags,
|
||||||
|
required final Map<int, StoragePath> storagePaths}) =
|
||||||
|
_$_DocumentBulkActionState;
|
||||||
|
const _DocumentBulkActionState._() : super._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<DocumentModel> get selection;
|
||||||
|
@override
|
||||||
|
Map<int, Correspondent> get correspondents;
|
||||||
|
@override
|
||||||
|
Map<int, DocumentType> get documentTypes;
|
||||||
|
@override
|
||||||
|
Map<int, Tag> get tags;
|
||||||
|
@override
|
||||||
|
Map<int, StoragePath> get storagePaths;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$_DocumentBulkActionStateCopyWith<_$_DocumentBulkActionState>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
part of 'document_bulk_action_cubit.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DocumentBulkActionState with _$DocumentBulkActionState {
|
||||||
|
const DocumentBulkActionState._();
|
||||||
|
const factory DocumentBulkActionState({
|
||||||
|
required List<DocumentModel> selection,
|
||||||
|
required Map<int, Correspondent> correspondents,
|
||||||
|
required Map<int, DocumentType> documentTypes,
|
||||||
|
required Map<int, Tag> tags,
|
||||||
|
required Map<int, StoragePath> storagePaths,
|
||||||
|
}) = _DocumentBulkActionState;
|
||||||
|
|
||||||
|
Iterable<int> get selectedIds => selection.map((d) => d.id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
const BulkEditLabelBottomSheet({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.formFieldLabel,
|
||||||
|
required this.formFieldPrefixIcon,
|
||||||
|
required this.availableOptionsSelector,
|
||||||
|
required this.onSubmit,
|
||||||
|
this.initialValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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(),
|
||||||
|
name: "labelFormField",
|
||||||
|
options: widget.availableOptionsSelector(state),
|
||||||
|
labelText: widget.formFieldLabel,
|
||||||
|
prefixIcon: widget.formFieldPrefixIcon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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 ConfirmBulkModifyLabelDialog extends StatelessWidget {
|
||||||
|
final String content;
|
||||||
|
const ConfirmBulkModifyLabelDialog({
|
||||||
|
super.key,
|
||||||
|
required this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(S.of(context)!.confirmAction),
|
||||||
|
content: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
text: content,
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: "\n\n"),
|
||||||
|
TextSpan(
|
||||||
|
text: S.of(context)!.areYouSureYouWantToContinue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: const [
|
||||||
|
DialogCancelButton(),
|
||||||
|
DialogConfirmButton(
|
||||||
|
style: DialogConfirmButtonStyle.danger,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
|
import 'package:flutter/src/widgets/placeholder.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 ConfirmBulkModifyTagsDialog extends StatelessWidget {
|
||||||
|
final int selectionCount;
|
||||||
|
final List<String> removeTags;
|
||||||
|
final List<String> addTags;
|
||||||
|
const ConfirmBulkModifyTagsDialog({
|
||||||
|
super.key,
|
||||||
|
required this.removeTags,
|
||||||
|
required this.addTags,
|
||||||
|
required this.selectionCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(S.of(context)!.confirmAction),
|
||||||
|
content: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
text: _buildText(context),
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: "\n\n"),
|
||||||
|
TextSpan(
|
||||||
|
text: S.of(context)!.areYouSureYouWantToContinue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: const [
|
||||||
|
DialogCancelButton(),
|
||||||
|
DialogConfirmButton(
|
||||||
|
style: DialogConfirmButtonStyle.danger,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildText(BuildContext context) {
|
||||||
|
if (removeTags.isNotEmpty && addTags.isNotEmpty) {
|
||||||
|
return S.of(context)!.bulkEditTagsModifyMessage(
|
||||||
|
addTags.join(", "),
|
||||||
|
selectionCount,
|
||||||
|
removeTags.join(", "),
|
||||||
|
);
|
||||||
|
} else if (removeTags.isNotEmpty) {
|
||||||
|
return S.of(context)!.bulkEditTagsRemoveMessage(
|
||||||
|
selectionCount,
|
||||||
|
removeTags.join(", "),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return S.of(context)!.bulkEditTagsAddMessage(
|
||||||
|
selectionCount,
|
||||||
|
addTags.join(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.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/features/document_bulk_action/view/widgets/confirm_bulk_modify_label_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class FullscreenBulkEditLabelPage extends StatefulWidget {
|
||||||
|
final String hintText;
|
||||||
|
final Map<int, Label> options;
|
||||||
|
final List<DocumentModel> selection;
|
||||||
|
final int? Function(DocumentModel document) labelMapper;
|
||||||
|
final Widget leadingIcon;
|
||||||
|
final void Function(int? id) onSubmit;
|
||||||
|
final String Function(int count) removeMessageBuilder;
|
||||||
|
final String Function(int count, String name) assignMessageBuilder;
|
||||||
|
|
||||||
|
FullscreenBulkEditLabelPage({
|
||||||
|
super.key,
|
||||||
|
required this.options,
|
||||||
|
required this.selection,
|
||||||
|
required this.labelMapper,
|
||||||
|
required this.leadingIcon,
|
||||||
|
required this.hintText,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.removeMessageBuilder,
|
||||||
|
required this.assignMessageBuilder,
|
||||||
|
}) : assert(selection.isNotEmpty);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FullscreenBulkEditLabelPage> createState() =>
|
||||||
|
_FullscreenBulkEditLabelPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FullscreenBulkEditLabelPageState<T extends Label>
|
||||||
|
extends State<FullscreenBulkEditLabelPage> {
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
|
||||||
|
LabelSelection? _selection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.addListener(() {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
if (_initialValues.length == 1 && _initialValues.first != null) {
|
||||||
|
_selection = LabelSelection(_initialValues.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int?> get _initialValues =>
|
||||||
|
widget.selection.map(widget.labelMapper).toSet().toList();
|
||||||
|
|
||||||
|
Iterable<int> _generateOrderedLabels() sync* {
|
||||||
|
final _availableValues = widget.options.values
|
||||||
|
.where(
|
||||||
|
(e) => e.name.normalized().contains(_controller.text.normalized()))
|
||||||
|
.map((e) => e.id!)
|
||||||
|
.toSet();
|
||||||
|
for (var label
|
||||||
|
in _initialValues.toSet().intersection(_availableValues.toSet())) {
|
||||||
|
if (label != null) {
|
||||||
|
yield label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final id
|
||||||
|
in _availableValues.whereNot((e) => _initialValues.contains(e))) {
|
||||||
|
yield id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final _labels = _generateOrderedLabels();
|
||||||
|
final hideFab = _selection == null ||
|
||||||
|
(_initialValues.length == 1 &&
|
||||||
|
_selection?.label == _initialValues.first);
|
||||||
|
return FullscreenSelectionForm(
|
||||||
|
controller: _controller,
|
||||||
|
hintText: widget.hintText,
|
||||||
|
leadingIcon: widget.leadingIcon,
|
||||||
|
selectionBuilder: (context, index) =>
|
||||||
|
_buildItem(widget.options[_labels.elementAt(index)]!),
|
||||||
|
selectionCount: _labels.length,
|
||||||
|
floatingActionButton: !hideFab
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
onPressed: _onSubmit,
|
||||||
|
label: Text(S.of(context)!.apply),
|
||||||
|
icon: const Icon(Icons.done),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(Label label) {
|
||||||
|
Widget? trailingIcon;
|
||||||
|
if (_initialValues.length > 1 &&
|
||||||
|
_selection == null &&
|
||||||
|
_initialValues.contains(label.id)) {
|
||||||
|
trailingIcon = const Icon(Icons.remove);
|
||||||
|
} else if (_selection?.label == label.id) {
|
||||||
|
trailingIcon = const Icon(Icons.done);
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
title: Text(label.name),
|
||||||
|
trailing: trailingIcon,
|
||||||
|
onTap: () {
|
||||||
|
if (_selection?.label == label.id) {
|
||||||
|
setState(() {
|
||||||
|
_selection = LabelSelection(null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_selection = LabelSelection(label.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSubmit() async {
|
||||||
|
if (_selection == null) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
bool shouldPerformAction;
|
||||||
|
if (_selection!.label == null) {
|
||||||
|
shouldPerformAction = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ConfirmBulkModifyLabelDialog(
|
||||||
|
content: widget.removeMessageBuilder(widget.selection.length),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
} else {
|
||||||
|
final labelName = widget.options[_selection!.label]!.name;
|
||||||
|
shouldPerformAction = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ConfirmBulkModifyLabelDialog(
|
||||||
|
content: widget.assignMessageBuilder(
|
||||||
|
widget.selection.length,
|
||||||
|
'"$labelName"',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
if (shouldPerformAction) {
|
||||||
|
widget.onSubmit(_selection!.label);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LabelSelection {
|
||||||
|
final int? label;
|
||||||
|
|
||||||
|
LabelSelection(this.label);
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/dart_extensions.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/confirm_bulk_modify_tags_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class FullscreenBulkEditTagsWidget extends StatefulWidget {
|
||||||
|
const FullscreenBulkEditTagsWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FullscreenBulkEditTagsWidget> createState() =>
|
||||||
|
_FullscreenBulkEditTagsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FullscreenBulkEditTagsWidgetState
|
||||||
|
extends State<FullscreenBulkEditTagsWidget> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
/// Tags shared by all documents
|
||||||
|
late final List<int> _sharedTags;
|
||||||
|
|
||||||
|
/// Tags not assigned to at least one document in the selection
|
||||||
|
late final List<int> _nonSharedTags;
|
||||||
|
|
||||||
|
List<int> _addTags = [];
|
||||||
|
List<int> _removeTags = [];
|
||||||
|
late List<int> _filteredTags;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final state = context.read<DocumentBulkActionCubit>().state;
|
||||||
|
_sharedTags = state.selection
|
||||||
|
.map((e) => e.tags)
|
||||||
|
.map((e) => e.toSet())
|
||||||
|
.fold(
|
||||||
|
state.tags.values.map((e) => e.id!).toSet(),
|
||||||
|
(previousValue, element) => previousValue.intersection(element),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
_nonSharedTags = state.selection
|
||||||
|
.map((e) => e.tags)
|
||||||
|
.flattened
|
||||||
|
.toSet()
|
||||||
|
.difference(_sharedTags.toSet())
|
||||||
|
.toList();
|
||||||
|
_filteredTags = state.tags.keys.toList();
|
||||||
|
_controller.addListener(() {
|
||||||
|
setState(() {
|
||||||
|
_filteredTags = context
|
||||||
|
.read<DocumentBulkActionCubit>()
|
||||||
|
.state
|
||||||
|
.tags
|
||||||
|
.values
|
||||||
|
.where((e) =>
|
||||||
|
e.name.normalized().contains(_controller.text.normalized()))
|
||||||
|
.map((e) => e.id!)
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> get _assignedTags => [..._sharedTags, ..._nonSharedTags];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return FullscreenSelectionForm(
|
||||||
|
controller: _controller,
|
||||||
|
floatingActionButton: _addTags.isNotEmpty || _removeTags.isNotEmpty
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
label: Text(S.of(context)!.apply),
|
||||||
|
icon: const Icon(Icons.done),
|
||||||
|
onPressed: _submit,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
hintText: S.of(context)!.startTyping,
|
||||||
|
leadingIcon: const Icon(Icons.label_outline),
|
||||||
|
selectionBuilder: (context, index) {
|
||||||
|
return _buildTagOption(
|
||||||
|
_filteredTags[index],
|
||||||
|
state.tags,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectionCount: _filteredTags.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTagOption(int id, Map<int, Tag> options) {
|
||||||
|
Widget? icon;
|
||||||
|
if (_sharedTags.contains(id) && !_removeTags.contains(id)) {
|
||||||
|
// Tag is assigned to all documents and not marked for removal
|
||||||
|
// => will remain assigned
|
||||||
|
icon = const Icon(Icons.done);
|
||||||
|
} else if (_addTags.contains(id)) {
|
||||||
|
// tag is marked to be added
|
||||||
|
icon = const Icon(Icons.done);
|
||||||
|
} else if (_nonSharedTags.contains(id) && !_removeTags.contains(id)) {
|
||||||
|
// Tag is neither shared among all documents, nor marked to be removed or
|
||||||
|
// added but assigned to at least one document
|
||||||
|
icon = const Icon(Icons.remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(options[id]!.name),
|
||||||
|
trailing: icon,
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: options[id]!.color,
|
||||||
|
foregroundColor: options[id]!.textColor,
|
||||||
|
child: options[id]!.isInboxTag ? const Icon(Icons.inbox) : null,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (_addTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_addTags.remove(id);
|
||||||
|
});
|
||||||
|
if (_assignedTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_removeTags.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (_removeTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_removeTags.remove(id);
|
||||||
|
});
|
||||||
|
if (!_sharedTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_addTags.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_sharedTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_removeTags.add(id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_addTags.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit() async {
|
||||||
|
if (_addTags.isNotEmpty || _removeTags.isNotEmpty) {
|
||||||
|
final bloc = context.read<DocumentBulkActionCubit>();
|
||||||
|
final addNames = _addTags
|
||||||
|
.map((value) => "\"${bloc.state.tags[value]!.name}\"")
|
||||||
|
.toList();
|
||||||
|
final removeNames = _removeTags
|
||||||
|
.map((value) => "\"${bloc.state.tags[value]!.name}\"")
|
||||||
|
.toList();
|
||||||
|
final shouldPerformAction = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ConfirmBulkModifyTagsDialog(
|
||||||
|
selectionCount: bloc.state.selection.length,
|
||||||
|
addTags: addNames,
|
||||||
|
removeTags: removeNames,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (shouldPerformAction) {
|
||||||
|
bloc.bulkModifyTags(
|
||||||
|
removeTagIds: _removeTags,
|
||||||
|
addTagIds: _addTags,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// 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),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -2,32 +2,47 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:open_filex/open_filex.dart';
|
import 'package:open_filex/open_filex.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.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_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:path_provider/path_provider.dart';
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
part 'document_details_cubit.freezed.dart';
|
||||||
part 'document_details_state.dart';
|
part 'document_details_state.dart';
|
||||||
|
|
||||||
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||||
final PaperlessDocumentsApi _api;
|
final PaperlessDocumentsApi _api;
|
||||||
final DocumentChangedNotifier _notifier;
|
final DocumentChangedNotifier _notifier;
|
||||||
final LocalNotificationService _notificationService;
|
final LocalNotificationService _notificationService;
|
||||||
|
final LabelRepository _labelRepository;
|
||||||
|
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
|
||||||
DocumentDetailsCubit(
|
DocumentDetailsCubit(
|
||||||
this._api,
|
this._api,
|
||||||
|
this._labelRepository,
|
||||||
this._notifier,
|
this._notifier,
|
||||||
this._notificationService, {
|
this._notificationService, {
|
||||||
required DocumentModel initialDocument,
|
required DocumentModel initialDocument,
|
||||||
}) : super(DocumentDetailsState(document: initialDocument)) {
|
}) : super(DocumentDetailsState(
|
||||||
_notifier.subscribe(this, onUpdated: replace);
|
document: initialDocument,
|
||||||
|
)) {
|
||||||
|
_notifier.addListener(this, onUpdated: replace);
|
||||||
|
_labelRepository.addListener(
|
||||||
|
this,
|
||||||
|
onChanged: (labels) => emit(
|
||||||
|
state.copyWith(
|
||||||
|
correspondents: labels.correspondents,
|
||||||
|
documentTypes: labels.documentTypes,
|
||||||
|
tags: labels.tags,
|
||||||
|
storagePaths: labels.storagePaths,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
loadSuggestions();
|
loadSuggestions();
|
||||||
loadMetaData();
|
loadMetaData();
|
||||||
}
|
}
|
||||||
@@ -39,12 +54,16 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
|
|
||||||
Future<void> loadSuggestions() async {
|
Future<void> loadSuggestions() async {
|
||||||
final suggestions = await _api.findSuggestions(state.document);
|
final suggestions = await _api.findSuggestions(state.document);
|
||||||
emit(state.copyWith(suggestions: suggestions));
|
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);
|
||||||
emit(state.copyWith(metaData: metaData));
|
if (!isClosed) {
|
||||||
|
emit(state.copyWith(metaData: metaData));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadFullContent() async {
|
Future<void> loadFullContent() async {
|
||||||
@@ -70,8 +89,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
_notifier.notifyUpdated(updatedDocument);
|
_notifier.notifyUpdated(updatedDocument);
|
||||||
} else {
|
} else {
|
||||||
final int autoAsn = await _api.findNextAsn();
|
final int autoAsn = await _api.findNextAsn();
|
||||||
final updatedDocument = await _api
|
final updatedDocument =
|
||||||
.update(document.copyWith(archiveSerialNumber: () => autoAsn));
|
await _api.update(document.copyWith(archiveSerialNumber: () => autoAsn));
|
||||||
_notifier.notifyUpdated(updatedDocument);
|
_notifier.notifyUpdated(updatedDocument);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,8 +101,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
if (state.metaData == null) {
|
if (state.metaData == null) {
|
||||||
await loadMetaData();
|
await loadMetaData();
|
||||||
}
|
}
|
||||||
final desc = FileDescription.fromPath(
|
final desc = FileDescription.fromPath(state.metaData!.mediaFilename.replaceAll("/", " "));
|
||||||
state.metaData!.mediaFilename.replaceAll("/", " "));
|
|
||||||
|
|
||||||
final fileName = "${desc.filename}.pdf";
|
final fileName = "${desc.filename}.pdf";
|
||||||
final file = File("${cacheDir.path}/$fileName");
|
final file = File("${cacheDir.path}/$fileName");
|
||||||
@@ -117,8 +135,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
await FileService.downloadsDirectory,
|
await FileService.downloadsDirectory,
|
||||||
);
|
);
|
||||||
final desc = FileDescription.fromPath(
|
final desc = FileDescription.fromPath(
|
||||||
state.metaData!.mediaFilename
|
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure
|
||||||
.replaceAll("/", " "), // Flatten directory structure
|
|
||||||
);
|
);
|
||||||
if (!File(filePath).existsSync()) {
|
if (!File(filePath).existsSync()) {
|
||||||
File(filePath).createSync();
|
File(filePath).createSync();
|
||||||
@@ -183,8 +200,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
|
|
||||||
String _buildDownloadFilePath(bool original, Directory dir) {
|
String _buildDownloadFilePath(bool original, Directory dir) {
|
||||||
final description = FileDescription.fromPath(
|
final description = FileDescription.fromPath(
|
||||||
state.metaData!.mediaFilename
|
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure
|
||||||
.replaceAll("/", " "), // Flatten directory structure
|
|
||||||
);
|
);
|
||||||
final extension = original ? description.extension : 'pdf';
|
final extension = original ? description.extension : 'pdf';
|
||||||
return "${dir.path}/${description.filename}.$extension";
|
return "${dir.path}/${description.filename}.$extension";
|
||||||
@@ -192,10 +208,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
for (final element in _subscriptions) {
|
_labelRepository.removeListener(this);
|
||||||
await element.cancel();
|
_notifier.removeListener(this);
|
||||||
}
|
|
||||||
_notifier.unsubscribe(this);
|
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,350 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'document_details_cubit.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$DocumentDetailsState {
|
||||||
|
DocumentModel get document => throw _privateConstructorUsedError;
|
||||||
|
DocumentMetaData? get metaData => throw _privateConstructorUsedError;
|
||||||
|
bool get isFullContentLoaded => throw _privateConstructorUsedError;
|
||||||
|
String? get fullContent => throw _privateConstructorUsedError;
|
||||||
|
FieldSuggestions? get suggestions => throw _privateConstructorUsedError;
|
||||||
|
Map<int, Correspondent> get correspondents =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, DocumentType> get documentTypes =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, Tag> get tags => throw _privateConstructorUsedError;
|
||||||
|
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$DocumentDetailsStateCopyWith<DocumentDetailsState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $DocumentDetailsStateCopyWith<$Res> {
|
||||||
|
factory $DocumentDetailsStateCopyWith(DocumentDetailsState value,
|
||||||
|
$Res Function(DocumentDetailsState) then) =
|
||||||
|
_$DocumentDetailsStateCopyWithImpl<$Res, DocumentDetailsState>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{DocumentModel document,
|
||||||
|
DocumentMetaData? metaData,
|
||||||
|
bool isFullContentLoaded,
|
||||||
|
String? fullContent,
|
||||||
|
FieldSuggestions? suggestions,
|
||||||
|
Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, Tag> tags,
|
||||||
|
Map<int, StoragePath> storagePaths});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$DocumentDetailsStateCopyWithImpl<$Res,
|
||||||
|
$Val extends DocumentDetailsState>
|
||||||
|
implements $DocumentDetailsStateCopyWith<$Res> {
|
||||||
|
_$DocumentDetailsStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? document = null,
|
||||||
|
Object? metaData = freezed,
|
||||||
|
Object? isFullContentLoaded = null,
|
||||||
|
Object? fullContent = freezed,
|
||||||
|
Object? suggestions = freezed,
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? tags = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
document: null == document
|
||||||
|
? _value.document
|
||||||
|
: document // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DocumentModel,
|
||||||
|
metaData: freezed == metaData
|
||||||
|
? _value.metaData
|
||||||
|
: metaData // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DocumentMetaData?,
|
||||||
|
isFullContentLoaded: null == isFullContentLoaded
|
||||||
|
? _value.isFullContentLoaded
|
||||||
|
: isFullContentLoaded // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
fullContent: freezed == fullContent
|
||||||
|
? _value.fullContent
|
||||||
|
: fullContent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
suggestions: freezed == suggestions
|
||||||
|
? _value.suggestions
|
||||||
|
: suggestions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as FieldSuggestions?,
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value.correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value.documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value.tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value.storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$_DocumentDetailsStateCopyWith<$Res>
|
||||||
|
implements $DocumentDetailsStateCopyWith<$Res> {
|
||||||
|
factory _$$_DocumentDetailsStateCopyWith(_$_DocumentDetailsState value,
|
||||||
|
$Res Function(_$_DocumentDetailsState) then) =
|
||||||
|
__$$_DocumentDetailsStateCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{DocumentModel document,
|
||||||
|
DocumentMetaData? metaData,
|
||||||
|
bool isFullContentLoaded,
|
||||||
|
String? fullContent,
|
||||||
|
FieldSuggestions? suggestions,
|
||||||
|
Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, Tag> tags,
|
||||||
|
Map<int, StoragePath> storagePaths});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$_DocumentDetailsStateCopyWithImpl<$Res>
|
||||||
|
extends _$DocumentDetailsStateCopyWithImpl<$Res, _$_DocumentDetailsState>
|
||||||
|
implements _$$_DocumentDetailsStateCopyWith<$Res> {
|
||||||
|
__$$_DocumentDetailsStateCopyWithImpl(_$_DocumentDetailsState _value,
|
||||||
|
$Res Function(_$_DocumentDetailsState) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? document = null,
|
||||||
|
Object? metaData = freezed,
|
||||||
|
Object? isFullContentLoaded = null,
|
||||||
|
Object? fullContent = freezed,
|
||||||
|
Object? suggestions = freezed,
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? tags = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$_DocumentDetailsState(
|
||||||
|
document: null == document
|
||||||
|
? _value.document
|
||||||
|
: document // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DocumentModel,
|
||||||
|
metaData: freezed == metaData
|
||||||
|
? _value.metaData
|
||||||
|
: metaData // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DocumentMetaData?,
|
||||||
|
isFullContentLoaded: null == isFullContentLoaded
|
||||||
|
? _value.isFullContentLoaded
|
||||||
|
: isFullContentLoaded // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
fullContent: freezed == fullContent
|
||||||
|
? _value.fullContent
|
||||||
|
: fullContent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
suggestions: freezed == suggestions
|
||||||
|
? _value.suggestions
|
||||||
|
: suggestions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as FieldSuggestions?,
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value._correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value._documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value._tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value._storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_DocumentDetailsState implements _DocumentDetailsState {
|
||||||
|
const _$_DocumentDetailsState(
|
||||||
|
{required this.document,
|
||||||
|
this.metaData,
|
||||||
|
this.isFullContentLoaded = false,
|
||||||
|
this.fullContent,
|
||||||
|
this.suggestions,
|
||||||
|
final Map<int, Correspondent> correspondents = const {},
|
||||||
|
final Map<int, DocumentType> documentTypes = const {},
|
||||||
|
final Map<int, Tag> tags = const {},
|
||||||
|
final Map<int, StoragePath> storagePaths = const {}})
|
||||||
|
: _correspondents = correspondents,
|
||||||
|
_documentTypes = documentTypes,
|
||||||
|
_tags = tags,
|
||||||
|
_storagePaths = storagePaths;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DocumentModel document;
|
||||||
|
@override
|
||||||
|
final DocumentMetaData? metaData;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool isFullContentLoaded;
|
||||||
|
@override
|
||||||
|
final String? fullContent;
|
||||||
|
@override
|
||||||
|
final FieldSuggestions? suggestions;
|
||||||
|
final Map<int, Correspondent> _correspondents;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, Correspondent> get correspondents {
|
||||||
|
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_correspondents);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, DocumentType> _documentTypes;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, DocumentType> get documentTypes {
|
||||||
|
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_documentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, Tag> _tags;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, Tag> get tags {
|
||||||
|
if (_tags is EqualUnmodifiableMapView) return _tags;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, StoragePath> _storagePaths;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, StoragePath> get storagePaths {
|
||||||
|
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_storagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DocumentDetailsState(document: $document, metaData: $metaData, isFullContentLoaded: $isFullContentLoaded, fullContent: $fullContent, suggestions: $suggestions, correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$_DocumentDetailsState &&
|
||||||
|
(identical(other.document, document) ||
|
||||||
|
other.document == document) &&
|
||||||
|
(identical(other.metaData, metaData) ||
|
||||||
|
other.metaData == metaData) &&
|
||||||
|
(identical(other.isFullContentLoaded, isFullContentLoaded) ||
|
||||||
|
other.isFullContentLoaded == isFullContentLoaded) &&
|
||||||
|
(identical(other.fullContent, fullContent) ||
|
||||||
|
other.fullContent == fullContent) &&
|
||||||
|
(identical(other.suggestions, suggestions) ||
|
||||||
|
other.suggestions == suggestions) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._correspondents, _correspondents) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._documentTypes, _documentTypes) &&
|
||||||
|
const DeepCollectionEquality().equals(other._tags, _tags) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._storagePaths, _storagePaths));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
document,
|
||||||
|
metaData,
|
||||||
|
isFullContentLoaded,
|
||||||
|
fullContent,
|
||||||
|
suggestions,
|
||||||
|
const DeepCollectionEquality().hash(_correspondents),
|
||||||
|
const DeepCollectionEquality().hash(_documentTypes),
|
||||||
|
const DeepCollectionEquality().hash(_tags),
|
||||||
|
const DeepCollectionEquality().hash(_storagePaths));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$_DocumentDetailsStateCopyWith<_$_DocumentDetailsState> get copyWith =>
|
||||||
|
__$$_DocumentDetailsStateCopyWithImpl<_$_DocumentDetailsState>(
|
||||||
|
this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _DocumentDetailsState implements DocumentDetailsState {
|
||||||
|
const factory _DocumentDetailsState(
|
||||||
|
{required final DocumentModel document,
|
||||||
|
final DocumentMetaData? metaData,
|
||||||
|
final bool isFullContentLoaded,
|
||||||
|
final String? fullContent,
|
||||||
|
final FieldSuggestions? suggestions,
|
||||||
|
final Map<int, Correspondent> correspondents,
|
||||||
|
final Map<int, DocumentType> documentTypes,
|
||||||
|
final Map<int, Tag> tags,
|
||||||
|
final Map<int, StoragePath> storagePaths}) = _$_DocumentDetailsState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DocumentModel get document;
|
||||||
|
@override
|
||||||
|
DocumentMetaData? get metaData;
|
||||||
|
@override
|
||||||
|
bool get isFullContentLoaded;
|
||||||
|
@override
|
||||||
|
String? get fullContent;
|
||||||
|
@override
|
||||||
|
FieldSuggestions? get suggestions;
|
||||||
|
@override
|
||||||
|
Map<int, Correspondent> get correspondents;
|
||||||
|
@override
|
||||||
|
Map<int, DocumentType> get documentTypes;
|
||||||
|
@override
|
||||||
|
Map<int, Tag> get tags;
|
||||||
|
@override
|
||||||
|
Map<int, StoragePath> get storagePaths;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$_DocumentDetailsStateCopyWith<_$_DocumentDetailsState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
@@ -1,42 +1,16 @@
|
|||||||
part of 'document_details_cubit.dart';
|
part of 'document_details_cubit.dart';
|
||||||
|
|
||||||
class DocumentDetailsState with EquatableMixin {
|
@freezed
|
||||||
final DocumentModel document;
|
class DocumentDetailsState with _$DocumentDetailsState {
|
||||||
final DocumentMetaData? metaData;
|
const factory DocumentDetailsState({
|
||||||
final bool isFullContentLoaded;
|
required DocumentModel document,
|
||||||
final String? fullContent;
|
|
||||||
final FieldSuggestions suggestions;
|
|
||||||
|
|
||||||
const DocumentDetailsState({
|
|
||||||
required this.document,
|
|
||||||
this.metaData,
|
|
||||||
this.suggestions = const FieldSuggestions(),
|
|
||||||
this.isFullContentLoaded = false,
|
|
||||||
this.fullContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [
|
|
||||||
document,
|
|
||||||
suggestions,
|
|
||||||
isFullContentLoaded,
|
|
||||||
fullContent,
|
|
||||||
metaData,
|
|
||||||
];
|
|
||||||
|
|
||||||
DocumentDetailsState copyWith({
|
|
||||||
DocumentModel? document,
|
|
||||||
FieldSuggestions? suggestions,
|
|
||||||
bool? isFullContentLoaded,
|
|
||||||
String? fullContent,
|
|
||||||
DocumentMetaData? metaData,
|
DocumentMetaData? metaData,
|
||||||
}) {
|
@Default(false) bool isFullContentLoaded,
|
||||||
return DocumentDetailsState(
|
String? fullContent,
|
||||||
document: document ?? this.document,
|
FieldSuggestions? suggestions,
|
||||||
suggestions: suggestions ?? this.suggestions,
|
@Default({}) Map<int, Correspondent> correspondents,
|
||||||
isFullContentLoaded: isFullContentLoaded ?? this.isFullContentLoaded,
|
@Default({}) Map<int, DocumentType> documentTypes,
|
||||||
fullContent: fullContent ?? this.fullContent,
|
@Default({}) Map<int, Tag> tags,
|
||||||
metaData: metaData ?? this.metaData,
|
@Default({}) Map<int, StoragePath> storagePaths,
|
||||||
);
|
}) = _DocumentDetailsState;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:open_filex/open_filex.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/colored_tab_bar.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/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/document_content_widget.dart';
|
import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart';
|
||||||
@@ -42,6 +42,8 @@ class DocumentDetailsPage extends StatefulWidget {
|
|||||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||||
late Future<DocumentMetaData> _metaData;
|
late Future<DocumentMetaData> _metaData;
|
||||||
static const double _itemSpacing = 24;
|
static const double _itemSpacing = 24;
|
||||||
|
|
||||||
|
final _pagingScrollController = ScrollController();
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -58,116 +60,104 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
Navigator.of(context)
|
Navigator.of(context).pop(context.read<DocumentDetailsCubit>().state.document);
|
||||||
.pop(context.read<DocumentDetailsCubit>().state.document);
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 4,
|
length: 4,
|
||||||
child: BlocListener<ConnectivityCubit, ConnectivityState>(
|
child: BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
|
||||||
!previous.isConnected && current.isConnected,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
_loadMetaData();
|
_loadMetaData();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
extendBodyBehindAppBar: false,
|
extendBodyBehindAppBar: false,
|
||||||
floatingActionButtonLocation:
|
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||||
FloatingActionButtonLocation.endDocked,
|
|
||||||
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
||||||
bottomNavigationBar: _buildBottomAppBar(),
|
bottomNavigationBar: _buildBottomAppBar(),
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
SliverAppBar(
|
SliverOverlapAbsorber(
|
||||||
title: Text(context
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
.watch<DocumentDetailsCubit>()
|
sliver: SliverAppBar(
|
||||||
.state
|
title: Text(context.watch<DocumentDetailsCubit>().state.document.title),
|
||||||
.document
|
leading: const BackButton(),
|
||||||
.title),
|
pinned: true,
|
||||||
leading: const BackButton(),
|
forceElevated: innerBoxIsScrolled,
|
||||||
pinned: true,
|
collapsedHeight: kToolbarHeight,
|
||||||
forceElevated: innerBoxIsScrolled,
|
expandedHeight: 250.0,
|
||||||
collapsedHeight: kToolbarHeight,
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
expandedHeight: 250.0,
|
background: Stack(
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
alignment: Alignment.topCenter,
|
||||||
background: Stack(
|
children: [
|
||||||
alignment: Alignment.topCenter,
|
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
children: [
|
builder: (context, state) => Positioned.fill(
|
||||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
child: DocumentPreview(
|
||||||
builder: (context, state) => Positioned.fill(
|
document: state.document,
|
||||||
child: DocumentPreview(
|
fit: BoxFit.cover,
|
||||||
document: state.document,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.fill(
|
|
||||||
top: 0,
|
|
||||||
child: Container(
|
|
||||||
height: 100,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Colors.black.withOpacity(0.7),
|
|
||||||
Colors.black.withOpacity(0.2),
|
|
||||||
Colors.transparent,
|
|
||||||
Colors.transparent,
|
|
||||||
],
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned.fill(
|
||||||
],
|
top: 0,
|
||||||
|
child: Container(
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.black.withOpacity(0.7),
|
||||||
|
Colors.black.withOpacity(0.2),
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
bottom: ColoredTabBar(
|
||||||
bottom: ColoredTabBar(
|
tabBar: TabBar(
|
||||||
tabBar: TabBar(
|
isScrollable: true,
|
||||||
isScrollable: true,
|
tabs: [
|
||||||
tabs: [
|
Tab(
|
||||||
Tab(
|
child: Text(
|
||||||
child: Text(
|
S.of(context)!.overview,
|
||||||
S.of(context)!.overview,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color: Theme.of(context)
|
),
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Tab(
|
||||||
Tab(
|
child: Text(
|
||||||
child: Text(
|
S.of(context)!.content,
|
||||||
S.of(context)!.content,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color: Theme.of(context)
|
),
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Tab(
|
||||||
Tab(
|
child: Text(
|
||||||
child: Text(
|
S.of(context)!.metaData,
|
||||||
S.of(context)!.metaData,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color: Theme.of(context)
|
),
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Tab(
|
||||||
Tab(
|
child: Text(
|
||||||
child: Text(
|
S.of(context)!.similarDocuments,
|
||||||
S.of(context)!.similarDocuments,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color: Theme.of(context)
|
),
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -176,29 +166,71 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => SimilarDocumentsCubit(
|
create: (context) => SimilarDocumentsCubit(
|
||||||
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
documentId: state.document.id,
|
documentId: state.document.id,
|
||||||
),
|
),
|
||||||
child: TabBarView(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(
|
||||||
DocumentOverviewWidget(
|
vertical: 16,
|
||||||
document: state.document,
|
horizontal: 16,
|
||||||
itemSpacing: _itemSpacing,
|
),
|
||||||
queryString: widget.titleAndContentQueryString,
|
child: TabBarView(
|
||||||
),
|
children: [
|
||||||
DocumentContentWidget(
|
CustomScrollView(
|
||||||
isFullContentLoaded: state.isFullContentLoaded,
|
slivers: [
|
||||||
document: state.document,
|
SliverOverlapInjector(
|
||||||
fullContent: state.fullContent,
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
queryString: widget.titleAndContentQueryString,
|
),
|
||||||
),
|
DocumentOverviewWidget(
|
||||||
DocumentMetaDataWidget(
|
document: state.document,
|
||||||
document: state.document,
|
itemSpacing: _itemSpacing,
|
||||||
itemSpacing: _itemSpacing,
|
queryString: widget.titleAndContentQueryString,
|
||||||
),
|
availableCorrespondents: state.correspondents,
|
||||||
const SimilarDocumentsView(),
|
availableDocumentTypes: state.documentTypes,
|
||||||
],
|
availableTags: state.tags,
|
||||||
|
availableStoragePaths: state.storagePaths,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
|
),
|
||||||
|
DocumentContentWidget(
|
||||||
|
isFullContentLoaded: state.isFullContentLoaded,
|
||||||
|
document: state.document,
|
||||||
|
fullContent: state.fullContent,
|
||||||
|
queryString: widget.titleAndContentQueryString,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
|
),
|
||||||
|
DocumentMetaDataWidget(
|
||||||
|
document: state.document,
|
||||||
|
itemSpacing: _itemSpacing,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CustomScrollView(
|
||||||
|
controller: _pagingScrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
|
),
|
||||||
|
SimilarDocumentsView(
|
||||||
|
pagingScrollController: _pagingScrollController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -213,32 +245,21 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
Widget _buildEditButton() {
|
Widget _buildEditButton() {
|
||||||
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final _filteredSuggestions =
|
// final _filteredSuggestions =
|
||||||
state.suggestions.documentDifference(state.document);
|
// state.suggestions?.documentDifference(state.document);
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
if (!connectivityState.isConnected) {
|
if (!connectivityState.isConnected) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return b.Badge(
|
return Tooltip(
|
||||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
message: S.of(context)!.editDocumentTooltip,
|
||||||
showBadge: _filteredSuggestions.hasSuggestions,
|
preferBelow: false,
|
||||||
child: Tooltip(
|
verticalOffset: 40,
|
||||||
message: S.of(context)!.editDocumentTooltip,
|
child: FloatingActionButton(
|
||||||
preferBelow: false,
|
child: const Icon(Icons.edit),
|
||||||
verticalOffset: 40,
|
onPressed: () => _onEdit(state.document),
|
||||||
child: FloatingActionButton(
|
|
||||||
child: const Icon(Icons.edit),
|
|
||||||
onPressed: () => _onEdit(state.document),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
badgeContent: Text(
|
|
||||||
'${_filteredSuggestions.suggestionsCount}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
badgeColor: Colors.red,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -259,9 +280,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
tooltip: S.of(context)!.deleteDocumentTooltip,
|
tooltip: S.of(context)!.deleteDocumentTooltip,
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: widget.allowEdit && isConnected
|
onPressed:
|
||||||
? () => _onDelete(state.document)
|
widget.allowEdit && isConnected ? () => _onDelete(state.document) : null,
|
||||||
: null,
|
|
||||||
).paddedSymmetrically(horizontal: 4),
|
).paddedSymmetrically(horizontal: 4),
|
||||||
DocumentDownloadButton(
|
DocumentDownloadButton(
|
||||||
document: state.document,
|
document: state.document,
|
||||||
@@ -271,8 +291,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
tooltip: S.of(context)!.previewTooltip,
|
tooltip: S.of(context)!.previewTooltip,
|
||||||
icon: const Icon(Icons.visibility),
|
icon: const Icon(Icons.visibility),
|
||||||
onPressed:
|
onPressed: isConnected ? () => _onOpen(state.document) : null,
|
||||||
isConnected ? () => _onOpen(state.document) : null,
|
|
||||||
).paddedOnly(right: 4.0),
|
).paddedOnly(right: 4.0),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: S.of(context)!.openInSystemViewer,
|
tooltip: S.of(context)!.openInSystemViewer,
|
||||||
@@ -299,13 +318,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
providers: [
|
providers: [
|
||||||
BlocProvider.value(
|
BlocProvider.value(
|
||||||
value: DocumentEditCubit(
|
value: DocumentEditCubit(
|
||||||
document,
|
context.read(),
|
||||||
documentsApi: context.read(),
|
context.read(),
|
||||||
correspondentRepository: context.read(),
|
context.read(),
|
||||||
documentTypeRepository: context.read(),
|
document: document,
|
||||||
storagePathRepository: context.read(),
|
|
||||||
tagRepository: context.read(),
|
|
||||||
notifier: context.read(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider<DocumentDetailsCubit>.value(
|
BlocProvider<DocumentDetailsCubit>.value(
|
||||||
@@ -313,8 +329,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: BlocListener<DocumentEditCubit, DocumentEditState>(
|
child: BlocListener<DocumentEditCubit, DocumentEditState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) => previous.document != current.document,
|
||||||
previous.document != current.document,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
cubit.replace(state.document);
|
cubit.replace(state.document);
|
||||||
},
|
},
|
||||||
@@ -334,8 +349,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onOpenFileInSystemViewer() async {
|
void _onOpenFileInSystemViewer() async {
|
||||||
final status =
|
final status = await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
|
||||||
await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
|
|
||||||
if (status == ResultType.done) return;
|
if (status == ResultType.done) return;
|
||||||
if (status == ResultType.noAppToOpen) {
|
if (status == ResultType.noAppToOpen) {
|
||||||
showGenericError(context, S.of(context)!.noAppToDisplayPDFFilesFound);
|
showGenericError(context, S.of(context)!.noAppToDisplayPDFFilesFound);
|
||||||
@@ -344,16 +358,14 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
showGenericError(context, translateError(context, ErrorCode.unknown));
|
showGenericError(context, translateError(context, ErrorCode.unknown));
|
||||||
}
|
}
|
||||||
if (status == ResultType.permissionDenied) {
|
if (status == ResultType.permissionDenied) {
|
||||||
showGenericError(
|
showGenericError(context, S.of(context)!.couldNotOpenFilePermissionDenied);
|
||||||
context, S.of(context)!.couldNotOpenFilePermissionDenied);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDelete(DocumentModel document) async {
|
void _onDelete(DocumentModel document) async {
|
||||||
final delete = await showDialog(
|
final delete = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) =>
|
builder: (context) => DeleteDocumentConfirmationDialog(document: document),
|
||||||
DeleteDocumentConfirmationDialog(document: document),
|
|
||||||
) ??
|
) ??
|
||||||
false;
|
false;
|
||||||
if (delete) {
|
if (delete) {
|
||||||
@@ -373,8 +385,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => DocumentView(
|
builder: (context) => DocumentView(
|
||||||
documentBytes:
|
documentBytes: context.read<PaperlessDocumentsApi>().getPreview(document.id),
|
||||||
context.read<PaperlessDocumentsApi>().getPreview(document.id),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ class DocumentContentWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return SliverToBoxAdapter(
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 16,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:paperless_api/paperless_api.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';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.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';
|
||||||
@@ -43,9 +43,8 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
width: 16,
|
width: 16,
|
||||||
)
|
)
|
||||||
: const Icon(Icons.download),
|
: const Icon(Icons.download),
|
||||||
onPressed: widget.document != null && widget.enabled
|
onPressed:
|
||||||
? () => _onDownload(widget.document!)
|
widget.document != null && widget.enabled ? () => _onDownload(widget.document!) : null,
|
||||||
: null,
|
|
||||||
).paddedOnly(right: 4);
|
).paddedOnly(right: 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +68,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
setState(() => _isDownloadPending = true);
|
setState(() => _isDownloadPending = true);
|
||||||
await context.read<DocumentDetailsCubit>().downloadDocument(
|
await context.read<DocumentDetailsCubit>().downloadDocument(
|
||||||
downloadOriginal: downloadOriginal,
|
downloadOriginal: downloadOriginal,
|
||||||
locale: context
|
locale: context.read<GlobalSettings>().preferredLocaleSubtag,
|
||||||
.read<ApplicationSettingsCubit>()
|
|
||||||
.state
|
|
||||||
.preferredLocaleSubtag,
|
|
||||||
);
|
);
|
||||||
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
|||||||
@@ -31,50 +31,43 @@ class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
|
|||||||
if (state.metaData == null) {
|
if (state.metaData == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
return SingleChildScrollView(
|
return SliverList(
|
||||||
child: Padding(
|
delegate: SliverChildListDelegate(
|
||||||
padding: const EdgeInsets.symmetric(
|
[
|
||||||
vertical: 16,
|
ArchiveSerialNumberField(
|
||||||
horizontal: 16,
|
document: widget.document,
|
||||||
),
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
child: Column(
|
DetailsItem.text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
DateFormat().format(widget.document.modified),
|
||||||
children: [
|
context: context,
|
||||||
ArchiveSerialNumberField(
|
label: S.of(context)!.modifiedAt,
|
||||||
document: widget.document,
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
DetailsItem.text(
|
||||||
DetailsItem.text(
|
DateFormat().format(widget.document.added),
|
||||||
DateFormat().format(widget.document.modified),
|
context: context,
|
||||||
context: context,
|
label: S.of(context)!.addedAt,
|
||||||
label: S.of(context)!.modifiedAt,
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
DetailsItem.text(
|
||||||
DetailsItem.text(
|
state.metaData!.mediaFilename,
|
||||||
DateFormat().format(widget.document.added),
|
context: context,
|
||||||
context: context,
|
label: S.of(context)!.mediaFilename,
|
||||||
label: S.of(context)!.addedAt,
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
DetailsItem.text(
|
||||||
DetailsItem.text(
|
state.metaData!.originalChecksum,
|
||||||
state.metaData!.mediaFilename,
|
context: context,
|
||||||
context: context,
|
label: S.of(context)!.originalMD5Checksum,
|
||||||
label: S.of(context)!.mediaFilename,
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
DetailsItem.text(
|
||||||
DetailsItem.text(
|
formatBytes(state.metaData!.originalSize, 2),
|
||||||
state.metaData!.originalChecksum,
|
context: context,
|
||||||
context: context,
|
label: S.of(context)!.originalFileSize,
|
||||||
label: S.of(context)!.originalMD5Checksum,
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
DetailsItem.text(
|
||||||
DetailsItem.text(
|
state.metaData!.originalMimeType,
|
||||||
formatBytes(state.metaData!.originalSize, 2),
|
context: context,
|
||||||
context: context,
|
label: S.of(context)!.originalMIMEType,
|
||||||
label: S.of(context)!.originalFileSize,
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
],
|
||||||
DetailsItem.text(
|
|
||||||
state.metaData!.originalMimeType,
|
|
||||||
context: context,
|
|
||||||
label: S.of(context)!.originalMIMEType,
|
|
||||||
).paddedOnly(bottom: widget.itemSpacing),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|||||||
|
|
||||||
class DocumentOverviewWidget extends StatelessWidget {
|
class DocumentOverviewWidget extends StatelessWidget {
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
|
final Map<int, Correspondent> availableCorrespondents;
|
||||||
|
final Map<int, DocumentType> availableDocumentTypes;
|
||||||
|
final Map<int, Tag> availableTags;
|
||||||
|
final Map<int, StoragePath> availableStoragePaths;
|
||||||
final String? queryString;
|
final String? queryString;
|
||||||
final double itemSpacing;
|
final double itemSpacing;
|
||||||
const DocumentOverviewWidget({
|
const DocumentOverviewWidget({
|
||||||
@@ -18,72 +22,74 @@ class DocumentOverviewWidget extends StatelessWidget {
|
|||||||
required this.document,
|
required this.document,
|
||||||
this.queryString,
|
this.queryString,
|
||||||
required this.itemSpacing,
|
required this.itemSpacing,
|
||||||
|
required this.availableCorrespondents,
|
||||||
|
required this.availableDocumentTypes,
|
||||||
|
required this.availableTags,
|
||||||
|
required this.availableStoragePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return SliverList(
|
||||||
padding: const EdgeInsets.symmetric(
|
delegate: SliverChildListDelegate(
|
||||||
vertical: 16,
|
[
|
||||||
horizontal: 16,
|
DetailsItem(
|
||||||
),
|
label: S.of(context)!.title,
|
||||||
children: [
|
content: HighlightedText(
|
||||||
DetailsItem(
|
text: document.title,
|
||||||
label: S.of(context)!.title,
|
highlights: queryString?.split(" ") ?? [],
|
||||||
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),
|
|
||||||
Visibility(
|
|
||||||
visible: document.documentType != null,
|
|
||||||
child: DetailsItem(
|
|
||||||
label: S.of(context)!.documentType,
|
|
||||||
content: LabelText<DocumentType>(
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
id: document.documentType,
|
|
||||||
),
|
),
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
),
|
DetailsItem.text(
|
||||||
Visibility(
|
DateFormat.yMMMMd().format(document.created),
|
||||||
visible: document.correspondent != null,
|
context: context,
|
||||||
child: DetailsItem(
|
label: S.of(context)!.createdAt,
|
||||||
label: S.of(context)!.correspondent,
|
|
||||||
content: LabelText<Correspondent>(
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
id: document.correspondent,
|
|
||||||
),
|
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
),
|
Visibility(
|
||||||
Visibility(
|
visible: document.documentType != null,
|
||||||
visible: document.storagePath != null,
|
child: DetailsItem(
|
||||||
child: DetailsItem(
|
label: S.of(context)!.documentType,
|
||||||
label: S.of(context)!.storagePath,
|
content: LabelText<DocumentType>(
|
||||||
content: StoragePathWidget(
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
pathId: document.storagePath,
|
label: availableDocumentTypes[document.documentType],
|
||||||
),
|
|
||||||
).paddedOnly(bottom: itemSpacing),
|
|
||||||
),
|
|
||||||
Visibility(
|
|
||||||
visible: document.tags.isNotEmpty,
|
|
||||||
child: DetailsItem(
|
|
||||||
label: S.of(context)!.tags,
|
|
||||||
content: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: TagsWidget(
|
|
||||||
isClickable: false,
|
|
||||||
tagIds: document.tags,
|
|
||||||
),
|
),
|
||||||
),
|
).paddedOnly(bottom: itemSpacing),
|
||||||
).paddedOnly(bottom: itemSpacing),
|
),
|
||||||
),
|
Visibility(
|
||||||
],
|
visible: document.correspondent != null,
|
||||||
|
child: DetailsItem(
|
||||||
|
label: S.of(context)!.correspondent,
|
||||||
|
content: LabelText<Correspondent>(
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
label: availableCorrespondents[document.correspondent],
|
||||||
|
),
|
||||||
|
).paddedOnly(bottom: itemSpacing),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: document.storagePath != null,
|
||||||
|
child: DetailsItem(
|
||||||
|
label: S.of(context)!.storagePath,
|
||||||
|
content: LabelText<StoragePath>(
|
||||||
|
label: availableStoragePaths[document.storagePath],
|
||||||
|
),
|
||||||
|
).paddedOnly(bottom: itemSpacing),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: document.tags.isNotEmpty,
|
||||||
|
child: 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,36 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
|
||||||
part 'document_edit_state.dart';
|
part 'document_edit_state.dart';
|
||||||
|
part 'document_edit_cubit.freezed.dart';
|
||||||
|
|
||||||
class DocumentEditCubit extends Cubit<DocumentEditState> {
|
class DocumentEditCubit extends Cubit<DocumentEditState> {
|
||||||
final DocumentModel _initialDocument;
|
final DocumentModel _initialDocument;
|
||||||
final PaperlessDocumentsApi _docsApi;
|
final PaperlessDocumentsApi _docsApi;
|
||||||
|
final LabelRepository _labelRepository;
|
||||||
final DocumentChangedNotifier _notifier;
|
final DocumentChangedNotifier _notifier;
|
||||||
final LabelRepository<Correspondent> _correspondentRepository;
|
|
||||||
final LabelRepository<DocumentType> _documentTypeRepository;
|
|
||||||
final LabelRepository<StoragePath> _storagePathRepository;
|
|
||||||
final LabelRepository<Tag> _tagRepository;
|
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
|
||||||
|
|
||||||
DocumentEditCubit(
|
DocumentEditCubit(
|
||||||
DocumentModel document, {
|
this._labelRepository,
|
||||||
required PaperlessDocumentsApi documentsApi,
|
this._docsApi,
|
||||||
required LabelRepository<Correspondent> correspondentRepository,
|
this._notifier, {
|
||||||
required LabelRepository<DocumentType> documentTypeRepository,
|
required DocumentModel document,
|
||||||
required LabelRepository<StoragePath> storagePathRepository,
|
|
||||||
required LabelRepository<Tag> tagRepository,
|
|
||||||
required DocumentChangedNotifier notifier,
|
|
||||||
}) : _initialDocument = document,
|
}) : _initialDocument = document,
|
||||||
_docsApi = documentsApi,
|
super(DocumentEditState(document: document)) {
|
||||||
_correspondentRepository = correspondentRepository,
|
_notifier.addListener(this, onUpdated: replace);
|
||||||
_documentTypeRepository = documentTypeRepository,
|
_labelRepository.addListener(
|
||||||
_storagePathRepository = storagePathRepository,
|
this,
|
||||||
_tagRepository = tagRepository,
|
onChanged: (labels) => emit(state.copyWith(
|
||||||
_notifier = notifier,
|
correspondents: labels.correspondents,
|
||||||
super(
|
documentTypes: labels.documentTypes,
|
||||||
DocumentEditState(
|
storagePaths: labels.storagePaths,
|
||||||
document: document,
|
tags: labels.tags,
|
||||||
correspondents: correspondentRepository.current?.values ?? {},
|
)),
|
||||||
documentTypes: documentTypeRepository.current?.values ?? {},
|
|
||||||
storagePaths: storagePathRepository.current?.values ?? {},
|
|
||||||
tags: tagRepository.current?.values ?? {},
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
_notifier.subscribe(this, onUpdated: replace);
|
|
||||||
_subscriptions.add(
|
|
||||||
_correspondentRepository.values
|
|
||||||
.listen((v) => emit(state.copyWith(correspondents: v?.values))),
|
|
||||||
);
|
|
||||||
_subscriptions.add(
|
|
||||||
_documentTypeRepository.values
|
|
||||||
.listen((v) => emit(state.copyWith(documentTypes: v?.values))),
|
|
||||||
);
|
|
||||||
_subscriptions.add(
|
|
||||||
_storagePathRepository.values
|
|
||||||
.listen((v) => emit(state.copyWith(storagePaths: v?.values))),
|
|
||||||
);
|
|
||||||
_subscriptions.add(
|
|
||||||
_tagRepository.values.listen(
|
|
||||||
(v) => emit(state.copyWith(tags: v?.values)),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,20 +40,20 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
|
|||||||
|
|
||||||
// Reload changed labels (documentCount property changes with removal/add)
|
// Reload changed labels (documentCount property changes with removal/add)
|
||||||
if (document.documentType != _initialDocument.documentType) {
|
if (document.documentType != _initialDocument.documentType) {
|
||||||
_documentTypeRepository
|
_labelRepository.findDocumentType(
|
||||||
.find((document.documentType ?? _initialDocument.documentType)!);
|
(document.documentType ?? _initialDocument.documentType)!);
|
||||||
}
|
}
|
||||||
if (document.correspondent != _initialDocument.correspondent) {
|
if (document.correspondent != _initialDocument.correspondent) {
|
||||||
_correspondentRepository
|
_labelRepository.findCorrespondent(
|
||||||
.find((document.correspondent ?? _initialDocument.correspondent)!);
|
(document.correspondent ?? _initialDocument.correspondent)!);
|
||||||
}
|
}
|
||||||
if (document.storagePath != _initialDocument.storagePath) {
|
if (document.storagePath != _initialDocument.storagePath) {
|
||||||
_storagePathRepository
|
_labelRepository.findStoragePath(
|
||||||
.find((document.storagePath ?? _initialDocument.storagePath)!);
|
(document.storagePath ?? _initialDocument.storagePath)!);
|
||||||
}
|
}
|
||||||
if (!const DeepCollectionEquality.unordered()
|
if (!const DeepCollectionEquality.unordered()
|
||||||
.equals(document.tags, _initialDocument.tags)) {
|
.equals(document.tags, _initialDocument.tags)) {
|
||||||
_tagRepository.findAll(document.tags);
|
_labelRepository.findAllTags(document.tags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,10 +63,8 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
for (final sub in _subscriptions) {
|
_notifier.removeListener(this);
|
||||||
sub.cancel();
|
_labelRepository.removeListener(this);
|
||||||
}
|
|
||||||
_notifier.unsubscribe(this);
|
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'document_edit_cubit.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$DocumentEditState {
|
||||||
|
DocumentModel get document => throw _privateConstructorUsedError;
|
||||||
|
Map<int, Correspondent> get correspondents =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, DocumentType> get documentTypes =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
|
||||||
|
Map<int, Tag> get tags => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$DocumentEditStateCopyWith<DocumentEditState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $DocumentEditStateCopyWith<$Res> {
|
||||||
|
factory $DocumentEditStateCopyWith(
|
||||||
|
DocumentEditState value, $Res Function(DocumentEditState) then) =
|
||||||
|
_$DocumentEditStateCopyWithImpl<$Res, DocumentEditState>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{DocumentModel document,
|
||||||
|
Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, StoragePath> storagePaths,
|
||||||
|
Map<int, Tag> tags});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$DocumentEditStateCopyWithImpl<$Res, $Val extends DocumentEditState>
|
||||||
|
implements $DocumentEditStateCopyWith<$Res> {
|
||||||
|
_$DocumentEditStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? document = null,
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
Object? tags = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
document: null == document
|
||||||
|
? _value.document
|
||||||
|
: document // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DocumentModel,
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value.correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value.documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value.storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value.tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$_DocumentEditStateCopyWith<$Res>
|
||||||
|
implements $DocumentEditStateCopyWith<$Res> {
|
||||||
|
factory _$$_DocumentEditStateCopyWith(_$_DocumentEditState value,
|
||||||
|
$Res Function(_$_DocumentEditState) then) =
|
||||||
|
__$$_DocumentEditStateCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{DocumentModel document,
|
||||||
|
Map<int, Correspondent> correspondents,
|
||||||
|
Map<int, DocumentType> documentTypes,
|
||||||
|
Map<int, StoragePath> storagePaths,
|
||||||
|
Map<int, Tag> tags});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$_DocumentEditStateCopyWithImpl<$Res>
|
||||||
|
extends _$DocumentEditStateCopyWithImpl<$Res, _$_DocumentEditState>
|
||||||
|
implements _$$_DocumentEditStateCopyWith<$Res> {
|
||||||
|
__$$_DocumentEditStateCopyWithImpl(
|
||||||
|
_$_DocumentEditState _value, $Res Function(_$_DocumentEditState) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? document = null,
|
||||||
|
Object? correspondents = null,
|
||||||
|
Object? documentTypes = null,
|
||||||
|
Object? storagePaths = null,
|
||||||
|
Object? tags = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$_DocumentEditState(
|
||||||
|
document: null == document
|
||||||
|
? _value.document
|
||||||
|
: document // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DocumentModel,
|
||||||
|
correspondents: null == correspondents
|
||||||
|
? _value._correspondents
|
||||||
|
: correspondents // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Correspondent>,
|
||||||
|
documentTypes: null == documentTypes
|
||||||
|
? _value._documentTypes
|
||||||
|
: documentTypes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, DocumentType>,
|
||||||
|
storagePaths: null == storagePaths
|
||||||
|
? _value._storagePaths
|
||||||
|
: storagePaths // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, StoragePath>,
|
||||||
|
tags: null == tags
|
||||||
|
? _value._tags
|
||||||
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<int, Tag>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_DocumentEditState implements _DocumentEditState {
|
||||||
|
const _$_DocumentEditState(
|
||||||
|
{required this.document,
|
||||||
|
final Map<int, Correspondent> correspondents = const {},
|
||||||
|
final Map<int, DocumentType> documentTypes = const {},
|
||||||
|
final Map<int, StoragePath> storagePaths = const {},
|
||||||
|
final Map<int, Tag> tags = const {}})
|
||||||
|
: _correspondents = correspondents,
|
||||||
|
_documentTypes = documentTypes,
|
||||||
|
_storagePaths = storagePaths,
|
||||||
|
_tags = tags;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DocumentModel document;
|
||||||
|
final Map<int, Correspondent> _correspondents;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, Correspondent> get correspondents {
|
||||||
|
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_correspondents);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, DocumentType> _documentTypes;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, DocumentType> get documentTypes {
|
||||||
|
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_documentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, StoragePath> _storagePaths;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, StoragePath> get storagePaths {
|
||||||
|
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_storagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<int, Tag> _tags;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<int, Tag> get tags {
|
||||||
|
if (_tags is EqualUnmodifiableMapView) return _tags;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DocumentEditState(document: $document, correspondents: $correspondents, documentTypes: $documentTypes, storagePaths: $storagePaths, tags: $tags)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$_DocumentEditState &&
|
||||||
|
(identical(other.document, document) ||
|
||||||
|
other.document == document) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._correspondents, _correspondents) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._documentTypes, _documentTypes) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._storagePaths, _storagePaths) &&
|
||||||
|
const DeepCollectionEquality().equals(other._tags, _tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
document,
|
||||||
|
const DeepCollectionEquality().hash(_correspondents),
|
||||||
|
const DeepCollectionEquality().hash(_documentTypes),
|
||||||
|
const DeepCollectionEquality().hash(_storagePaths),
|
||||||
|
const DeepCollectionEquality().hash(_tags));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$_DocumentEditStateCopyWith<_$_DocumentEditState> get copyWith =>
|
||||||
|
__$$_DocumentEditStateCopyWithImpl<_$_DocumentEditState>(
|
||||||
|
this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _DocumentEditState implements DocumentEditState {
|
||||||
|
const factory _DocumentEditState(
|
||||||
|
{required final DocumentModel document,
|
||||||
|
final Map<int, Correspondent> correspondents,
|
||||||
|
final Map<int, DocumentType> documentTypes,
|
||||||
|
final Map<int, StoragePath> storagePaths,
|
||||||
|
final Map<int, Tag> tags}) = _$_DocumentEditState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DocumentModel get document;
|
||||||
|
@override
|
||||||
|
Map<int, Correspondent> get correspondents;
|
||||||
|
@override
|
||||||
|
Map<int, DocumentType> get documentTypes;
|
||||||
|
@override
|
||||||
|
Map<int, StoragePath> get storagePaths;
|
||||||
|
@override
|
||||||
|
Map<int, Tag> get tags;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$_DocumentEditStateCopyWith<_$_DocumentEditState> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
@@ -1,43 +1,12 @@
|
|||||||
part of 'document_edit_cubit.dart';
|
part of 'document_edit_cubit.dart';
|
||||||
|
|
||||||
class DocumentEditState extends Equatable {
|
@freezed
|
||||||
final DocumentModel document;
|
class DocumentEditState with _$DocumentEditState {
|
||||||
|
const factory DocumentEditState({
|
||||||
final Map<int, Correspondent> correspondents;
|
required DocumentModel document,
|
||||||
final Map<int, DocumentType> documentTypes;
|
@Default({}) Map<int, Correspondent> correspondents,
|
||||||
final Map<int, StoragePath> storagePaths;
|
@Default({}) Map<int, DocumentType> documentTypes,
|
||||||
final Map<int, Tag> tags;
|
@Default({}) Map<int, StoragePath> storagePaths,
|
||||||
|
@Default({}) Map<int, Tag> tags,
|
||||||
const DocumentEditState({
|
}) = _DocumentEditState;
|
||||||
required this.correspondents,
|
|
||||||
required this.documentTypes,
|
|
||||||
required this.storagePaths,
|
|
||||||
required this.tags,
|
|
||||||
required this.document,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [
|
|
||||||
correspondents,
|
|
||||||
documentTypes,
|
|
||||||
storagePaths,
|
|
||||||
tags,
|
|
||||||
document,
|
|
||||||
];
|
|
||||||
|
|
||||||
DocumentEditState copyWith({
|
|
||||||
Map<int, Correspondent>? correspondents,
|
|
||||||
Map<int, DocumentType>? documentTypes,
|
|
||||||
Map<int, StoragePath>? storagePaths,
|
|
||||||
Map<int, Tag>? tags,
|
|
||||||
DocumentModel? document,
|
|
||||||
}) {
|
|
||||||
return DocumentEditState(
|
|
||||||
document: document ?? this.document,
|
|
||||||
correspondents: correspondents ?? this.correspondents,
|
|
||||||
documentTypes: documentTypes ?? this.documentTypes,
|
|
||||||
storagePaths: storagePaths ?? this.storagePaths,
|
|
||||||
tags: tags ?? this.tags,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
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:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
@@ -15,13 +16,14 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent
|
|||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_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_storage_path_page.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/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;
|
final FieldSuggestions? suggestions;
|
||||||
const DocumentEditPage({
|
const DocumentEditPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.suggestions,
|
required this.suggestions,
|
||||||
@@ -43,13 +45,13 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||||
bool _isSubmitLoading = false;
|
bool _isSubmitLoading = false;
|
||||||
|
|
||||||
late final FieldSuggestions _filteredSuggestions;
|
late final FieldSuggestions? _filteredSuggestions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_filteredSuggestions = widget.suggestions
|
_filteredSuggestions =
|
||||||
.documentDifference(context.read<DocumentEditCubit>().state.document);
|
widget.suggestions?.documentDifference(context.read<DocumentEditCubit>().state.document);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -93,69 +95,137 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
ListView(
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildTitleFormField(state.document.title).padded(),
|
_buildTitleFormField(state.document.title).padded(),
|
||||||
_buildCreatedAtFormField(state.document.created)
|
_buildCreatedAtFormField(state.document.created).padded(),
|
||||||
.padded(),
|
// Correspondent form field
|
||||||
_buildCorrespondentFormField(
|
Column(
|
||||||
state.document.correspondent,
|
children: [
|
||||||
state.correspondents,
|
LabelFormField<Correspondent>(
|
||||||
).padded(),
|
showAnyAssignedOption: false,
|
||||||
_buildDocumentTypeFormField(
|
showNotAssignedOption: false,
|
||||||
state.document.documentType,
|
addLabelPageBuilder: (initialValue) => RepositoryProvider.value(
|
||||||
state.documentTypes,
|
value: context.read<LabelRepository>(),
|
||||||
).padded(),
|
child: AddCorrespondentPage(
|
||||||
_buildStoragePathFormField(
|
initialName: initialValue,
|
||||||
state.document.storagePath,
|
),
|
||||||
state.storagePaths,
|
),
|
||||||
).padded(),
|
addLabelText: S.of(context)!.addCorrespondent,
|
||||||
TagFormField(
|
labelText: S.of(context)!.correspondent,
|
||||||
initialValue: IdsTagsQuery.included(
|
options: context.watch<DocumentEditCubit>().state.correspondents,
|
||||||
state.document.tags.toList()),
|
initialValue: state.document.correspondent != null
|
||||||
notAssignedSelectable: false,
|
? IdQueryParameter.fromId(state.document.correspondent!)
|
||||||
anyAssignedSelectable: false,
|
: const IdQueryParameter.unset(),
|
||||||
excludeAllowed: false,
|
name: fkCorrespondent,
|
||||||
name: fkTags,
|
prefixIcon: const Icon(Icons.person_outlined),
|
||||||
selectableOptions: state.tags,
|
),
|
||||||
suggestions: _filteredSuggestions.tags
|
if (_filteredSuggestions?.hasSuggestedCorrespondents ?? false)
|
||||||
.toSet()
|
_buildSuggestionsSkeleton<int>(
|
||||||
.difference(state.document.tags.toSet())
|
suggestions: _filteredSuggestions!.correspondents,
|
||||||
.isNotEmpty
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
? _buildSuggestionsSkeleton<int>(
|
label: Text(state.correspondents[itemData]!.name),
|
||||||
suggestions: _filteredSuggestions.tags,
|
onPressed: () {
|
||||||
itemBuilder: (context, itemData) {
|
_formKey.currentState?.fields[fkCorrespondent]?.didChange(
|
||||||
final tag = state.tags[itemData]!;
|
IdQueryParameter.fromId(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;
|
|
||||||
if (currentTags is IdsTagsQuery) {
|
|
||||||
_formKey
|
|
||||||
.currentState?.fields[fkTags]
|
|
||||||
?.didChange(
|
|
||||||
(IdsTagsQuery.fromIds({
|
|
||||||
...currentTags.ids,
|
|
||||||
itemData
|
|
||||||
})));
|
|
||||||
} else {
|
|
||||||
_formKey
|
|
||||||
.currentState?.fields[fkTags]
|
|
||||||
?.didChange(
|
|
||||||
(IdsTagsQuery.fromIds(
|
|
||||||
{itemData})));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
: null,
|
),
|
||||||
|
],
|
||||||
).padded(),
|
).padded(),
|
||||||
|
// DocumentType form field
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
LabelFormField<DocumentType>(
|
||||||
|
showAnyAssignedOption: false,
|
||||||
|
showNotAssignedOption: false,
|
||||||
|
addLabelPageBuilder: (currentInput) => RepositoryProvider.value(
|
||||||
|
value: context.read<LabelRepository>(),
|
||||||
|
child: AddDocumentTypePage(
|
||||||
|
initialName: currentInput,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).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
|
// Prevent tags from being hidden by fab
|
||||||
const SizedBox(height: 64),
|
const SizedBox(height: 64),
|
||||||
],
|
],
|
||||||
@@ -185,104 +255,18 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStoragePathFormField(
|
|
||||||
int? initialId,
|
|
||||||
Map<int, StoragePath> options,
|
|
||||||
) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
LabelFormField<StoragePath>(
|
|
||||||
notAssignedSelectable: false,
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
|
||||||
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
|
||||||
child: AddStoragePathPage(initalValue: initialValue),
|
|
||||||
),
|
|
||||||
textFieldLabel: S.of(context)!.storagePath,
|
|
||||||
labelOptions: options,
|
|
||||||
initialValue: IdQueryParameter.fromId(initialId),
|
|
||||||
name: fkStoragePath,
|
|
||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCorrespondentFormField(
|
|
||||||
int? initialId, Map<int, Correspondent> options) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
LabelFormField<Correspondent>(
|
|
||||||
notAssignedSelectable: false,
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
|
||||||
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
|
||||||
child: AddCorrespondentPage(initialName: initialValue),
|
|
||||||
),
|
|
||||||
textFieldLabel: S.of(context)!.correspondent,
|
|
||||||
labelOptions: options,
|
|
||||||
initialValue: IdQueryParameter.fromId(initialId),
|
|
||||||
name: fkCorrespondent,
|
|
||||||
prefixIcon: const Icon(Icons.person_outlined),
|
|
||||||
),
|
|
||||||
if (_filteredSuggestions.hasSuggestedCorrespondents)
|
|
||||||
_buildSuggestionsSkeleton<int>(
|
|
||||||
suggestions: _filteredSuggestions.correspondents,
|
|
||||||
itemBuilder: (context, itemData) => ActionChip(
|
|
||||||
label: Text(options[itemData]!.name),
|
|
||||||
onPressed: () => _formKey.currentState?.fields[fkCorrespondent]
|
|
||||||
?.didChange((IdQueryParameter.fromId(itemData))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDocumentTypeFormField(
|
|
||||||
int? initialId,
|
|
||||||
Map<int, DocumentType> options,
|
|
||||||
) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
LabelFormField<DocumentType>(
|
|
||||||
notAssignedSelectable: false,
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
|
|
||||||
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
|
||||||
child: AddDocumentTypePage(
|
|
||||||
initialName: currentInput,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
textFieldLabel: S.of(context)!.documentType,
|
|
||||||
initialValue: IdQueryParameter.fromId(initialId),
|
|
||||||
labelOptions: options,
|
|
||||||
name: fkDocumentType,
|
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
|
||||||
),
|
|
||||||
if (_filteredSuggestions.hasSuggestedDocumentTypes)
|
|
||||||
_buildSuggestionsSkeleton<int>(
|
|
||||||
suggestions: _filteredSuggestions.documentTypes,
|
|
||||||
itemBuilder: (context, itemData) => ActionChip(
|
|
||||||
label: Text(options[itemData]!.name),
|
|
||||||
onPressed: () => _formKey.currentState?.fields[fkDocumentType]
|
|
||||||
?.didChange(IdQueryParameter.fromId(itemData)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onSubmit(DocumentModel document) async {
|
Future<void> _onSubmit(DocumentModel document) async {
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
final values = _formKey.currentState!.value;
|
final values = _formKey.currentState!.value;
|
||||||
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).id,
|
documentType: () => (values[fkDocumentType] as SetIdQueryParameter).id,
|
||||||
correspondent: () => (values[fkCorrespondent] as IdQueryParameter).id,
|
correspondent: () => (values[fkCorrespondent] as SetIdQueryParameter).id,
|
||||||
storagePath: () => (values[fkStoragePath] as IdQueryParameter).id,
|
storagePath: () => (values[fkStoragePath] as SetIdQueryParameter).id,
|
||||||
tags: (values[fkTags] as IdsTagsQuery).includedIds,
|
tags: (values[fkTags] as IdsTagsQuery).include,
|
||||||
content: values[fkContent]);
|
content: values[fkContent],
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSubmitLoading = true;
|
_isSubmitLoading = true;
|
||||||
});
|
});
|
||||||
@@ -303,7 +287,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
Widget _buildTitleFormField(String? initialTitle) {
|
Widget _buildTitleFormField(String? initialTitle) {
|
||||||
return FormBuilderTextField(
|
return FormBuilderTextField(
|
||||||
name: fkTitle,
|
name: fkTitle,
|
||||||
validator: FormBuilderValidators.required(),
|
validator: (value) {
|
||||||
|
if (value?.trim().isEmpty ?? true) {
|
||||||
|
return S.of(context)!.thisFieldIsRequired;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
label: Text(S.of(context)!.title),
|
label: Text(S.of(context)!.title),
|
||||||
),
|
),
|
||||||
@@ -326,13 +315,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
format: DateFormat.yMMMMd(),
|
format: DateFormat.yMMMMd(),
|
||||||
initialEntryMode: DatePickerEntryMode.calendar,
|
initialEntryMode: DatePickerEntryMode.calendar,
|
||||||
),
|
),
|
||||||
if (_filteredSuggestions.hasSuggestedDates)
|
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]?.didChange(itemData),
|
||||||
?.didChange(itemData),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -361,11 +349,63 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
itemBuilder: (context, index) => ColoredChipWrapper(
|
itemBuilder: (context, index) => ColoredChipWrapper(
|
||||||
child: itemBuilder(context, suggestions.elementAt(index)),
|
child: itemBuilder(context, suggestions.elementAt(index)),
|
||||||
),
|
),
|
||||||
separatorBuilder: (BuildContext context, int index) =>
|
separatorBuilder: (BuildContext context, int index) => const SizedBox(width: 4.0),
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padded();
|
).padded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// class SampleWidget extends StatefulWidget {
|
||||||
|
// const SampleWidget({super.key});
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<SampleWidget> createState() => _SampleWidgetState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class _SampleWidgetState extends State<SampleWidget> {
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return BlocBuilder<OptionsBloc, OptionsState>(
|
||||||
|
// builder: (context, state) {
|
||||||
|
// return OptionsFormField(
|
||||||
|
// options: state.options,
|
||||||
|
// onAddOption: (option) {
|
||||||
|
// // This will call the repository and will cause a new state containing the new option to be emitted.
|
||||||
|
// context.read<OptionsBloc>().addOption(option);
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class OptionsFormField extends StatefulWidget {
|
||||||
|
// final List<Option> options;
|
||||||
|
// final void Function(Option option) onAddOption;
|
||||||
|
|
||||||
|
|
||||||
|
// const OptionsFormField({
|
||||||
|
// super.key,
|
||||||
|
// required this.options,
|
||||||
|
// required this.onAddOption,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<OptionsFormField> createState() => _OptionsFormFieldState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class _OptionsFormFieldState extends State<OptionsFormField> {
|
||||||
|
// final TextEditingController _controller;
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return TextFormField(
|
||||||
|
// onTap: () async {
|
||||||
|
// // User creates new option...
|
||||||
|
// final Option option = await showOptionCreationForm();
|
||||||
|
// widget.onAddOption(option);
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.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/global/constants.dart';
|
import 'package:paperless_mobile/core/global/constants.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_description.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';
|
||||||
@@ -198,20 +197,14 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
);
|
);
|
||||||
final uploadResult = await Navigator.of(context).push<DocumentUploadResult>(
|
final uploadResult = await Navigator.of(context).push<DocumentUploadResult>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => LabelRepositoriesProvider(
|
builder: (_) => BlocProvider(
|
||||||
child: BlocProvider(
|
create: (context) => DocumentUploadCubit(
|
||||||
create: (context) => DocumentUploadCubit(
|
context.read(),
|
||||||
documentApi: context.read<PaperlessDocumentsApi>(),
|
context.read(),
|
||||||
correspondentRepository:
|
),
|
||||||
context.read<LabelRepository<Correspondent>>(),
|
child: DocumentUploadPreparationPage(
|
||||||
documentTypeRepository:
|
fileBytes: file.bytes,
|
||||||
context.read<LabelRepository<DocumentType>>(),
|
fileExtension: file.extension,
|
||||||
tagRepository: context.read<LabelRepository<Tag>>(),
|
|
||||||
),
|
|
||||||
child: DocumentUploadPreparationPage(
|
|
||||||
fileBytes: file.bytes,
|
|
||||||
fileExtension: file.extension,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -316,22 +309,16 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
}
|
}
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => LabelRepositoriesProvider(
|
builder: (_) => BlocProvider(
|
||||||
child: BlocProvider(
|
create: (context) => DocumentUploadCubit(
|
||||||
create: (context) => DocumentUploadCubit(
|
context.read(),
|
||||||
documentApi: context.read<PaperlessDocumentsApi>(),
|
context.read(),
|
||||||
correspondentRepository:
|
),
|
||||||
context.read<LabelRepository<Correspondent>>(),
|
child: DocumentUploadPreparationPage(
|
||||||
documentTypeRepository:
|
fileBytes: file.readAsBytesSync(),
|
||||||
context.read<LabelRepository<DocumentType>>(),
|
filename: fileDescription.filename,
|
||||||
tagRepository: context.read<LabelRepository<Tag>>(),
|
title: fileDescription.filename,
|
||||||
),
|
fileExtension: fileDescription.extension,
|
||||||
child: DocumentUploadPreparationPage(
|
|
||||||
fileBytes: file.readAsBytesSync(),
|
|
||||||
filename: fileDescription.filename,
|
|
||||||
title: fileDescription.filename,
|
|
||||||
fileExtension: fileDescription.extension,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,26 +1,46 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_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/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';
|
||||||
|
|
||||||
|
part 'document_search_cubit.g.dart';
|
||||||
part 'document_search_state.dart';
|
part 'document_search_state.dart';
|
||||||
|
|
||||||
part 'document_search_cubit.g.dart';
|
class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPagingBlocMixin {
|
||||||
|
|
||||||
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
|
||||||
with DocumentPagingBlocMixin {
|
|
||||||
@override
|
@override
|
||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
|
final LabelRepository _labelRepository;
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
DocumentSearchCubit(this.api, this.notifier)
|
final LocalUserAppState _userAppState;
|
||||||
: super(const DocumentSearchState()) {
|
DocumentSearchCubit(
|
||||||
notifier.subscribe(
|
this.api,
|
||||||
|
this.notifier,
|
||||||
|
this._labelRepository,
|
||||||
|
this._userAppState,
|
||||||
|
) : super(DocumentSearchState(searchHistory: _userAppState.documentSearchHistory)) {
|
||||||
|
_labelRepository.addListener(
|
||||||
|
this,
|
||||||
|
onChanged: (labels) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
correspondents: labels.correspondents,
|
||||||
|
documentTypes: labels.documentTypes,
|
||||||
|
tags: labels.tags,
|
||||||
|
storagePaths: labels.storagePaths,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
notifier.addListener(
|
||||||
this,
|
this,
|
||||||
onDeleted: remove,
|
onDeleted: remove,
|
||||||
onUpdated: replace,
|
onUpdated: replace,
|
||||||
@@ -42,11 +62,13 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
searchHistory: [
|
searchHistory: [
|
||||||
query,
|
query,
|
||||||
...state.searchHistory
|
...state.searchHistory.whereNot((previousQuery) => previousQuery == query)
|
||||||
.whereNot((previousQuery) => previousQuery == query)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
_userAppState
|
||||||
|
..documentSearchHistory = state.searchHistory
|
||||||
|
..save();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateViewType(ViewType viewType) {
|
void updateViewType(ViewType viewType) {
|
||||||
@@ -56,11 +78,12 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
|||||||
void removeHistoryEntry(String entry) {
|
void removeHistoryEntry(String entry) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
searchHistory: state.searchHistory
|
searchHistory: state.searchHistory.whereNot((element) => element == entry).toList(),
|
||||||
.whereNot((element) => element == entry)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
_userAppState
|
||||||
|
..documentSearchHistory = state.searchHistory
|
||||||
|
..save();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> suggest(String query) async {
|
Future<void> suggest(String query) async {
|
||||||
@@ -80,26 +103,22 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
emit(state.copyWith(
|
emit(
|
||||||
view: SearchView.suggestions,
|
state.copyWith(
|
||||||
suggestions: [],
|
view: SearchView.suggestions,
|
||||||
isLoading: false,
|
suggestions: [],
|
||||||
));
|
isLoading: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
notifier.unsubscribe(this);
|
notifier.removeListener(this);
|
||||||
|
_labelRepository.removeListener(this);
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DocumentSearchState? fromJson(Map<String, dynamic> json) {
|
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
||||||
return DocumentSearchState.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic>? toJson(DocumentSearchState state) {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,25 @@ class DocumentSearchState extends DocumentPagingState {
|
|||||||
final List<String> suggestions;
|
final List<String> suggestions;
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final ViewType viewType;
|
final ViewType viewType;
|
||||||
|
|
||||||
|
final Map<int, Correspondent> correspondents;
|
||||||
|
final Map<int, DocumentType> documentTypes;
|
||||||
|
final Map<int, Tag> tags;
|
||||||
|
final Map<int, StoragePath> storagePaths;
|
||||||
|
|
||||||
const DocumentSearchState({
|
const DocumentSearchState({
|
||||||
this.view = SearchView.suggestions,
|
this.view = SearchView.suggestions,
|
||||||
this.searchHistory = const [],
|
this.searchHistory = const [],
|
||||||
this.suggestions = const [],
|
this.suggestions = const [],
|
||||||
this.viewType = ViewType.detailed,
|
this.viewType = ViewType.detailed,
|
||||||
super.filter,
|
super.filter = const DocumentFilter(),
|
||||||
super.hasLoaded,
|
super.hasLoaded,
|
||||||
super.isLoading,
|
super.isLoading,
|
||||||
super.value,
|
super.value,
|
||||||
|
this.correspondents = const {},
|
||||||
|
this.documentTypes = const {},
|
||||||
|
this.tags = const {},
|
||||||
|
this.storagePaths = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,6 +41,10 @@ class DocumentSearchState extends DocumentPagingState {
|
|||||||
suggestions,
|
suggestions,
|
||||||
view,
|
view,
|
||||||
viewType,
|
viewType,
|
||||||
|
correspondents,
|
||||||
|
documentTypes,
|
||||||
|
tags,
|
||||||
|
storagePaths,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -57,6 +71,10 @@ class DocumentSearchState extends DocumentPagingState {
|
|||||||
List<String>? suggestions,
|
List<String>? suggestions,
|
||||||
SearchView? view,
|
SearchView? view,
|
||||||
ViewType? viewType,
|
ViewType? viewType,
|
||||||
|
Map<int, Correspondent>? correspondents,
|
||||||
|
Map<int, DocumentType>? documentTypes,
|
||||||
|
Map<int, Tag>? tags,
|
||||||
|
Map<int, StoragePath>? storagePaths,
|
||||||
}) {
|
}) {
|
||||||
return DocumentSearchState(
|
return DocumentSearchState(
|
||||||
value: value ?? this.value,
|
value: value ?? this.value,
|
||||||
@@ -67,6 +85,10 @@ class DocumentSearchState extends DocumentPagingState {
|
|||||||
view: view ?? this.view,
|
view: view ?? this.view,
|
||||||
suggestions: suggestions ?? this.suggestions,
|
suggestions: suggestions ?? this.suggestions,
|
||||||
viewType: viewType ?? this.viewType,
|
viewType: viewType ?? this.viewType,
|
||||||
|
correspondents: correspondents ?? this.correspondents,
|
||||||
|
documentTypes: documentTypes ?? this.documentTypes,
|
||||||
|
tags: tags ?? this.tags,
|
||||||
|
storagePaths: storagePaths ?? this.storagePaths,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,31 @@ import 'dart:async';
|
|||||||
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:hive/hive.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/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/features/settings/model/view_type.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/routes/document_details_route.dart';
|
import 'package:paperless_mobile/routes/document_details_route.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
Future<void> showDocumentSearchPage(BuildContext context) {
|
Future<void> showDocumentSearchPage(BuildContext context) {
|
||||||
|
final currentUser =
|
||||||
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
|
||||||
return Navigator.of(context).push(
|
return Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BlocProvider(
|
builder: (context) => BlocProvider(
|
||||||
create: (context) => DocumentSearchCubit(
|
create: (context) => DocumentSearchCubit(
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState).get(currentUser)!,
|
||||||
),
|
),
|
||||||
child: const DocumentSearchPage(),
|
child: const DocumentSearchPage(),
|
||||||
),
|
),
|
||||||
@@ -69,13 +76,14 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
controller: _queryController,
|
controller: _queryController,
|
||||||
onChanged: (query) {
|
onChanged: (query) {
|
||||||
_debounceTimer?.cancel();
|
_debounceTimer?.cancel();
|
||||||
_debounceTimer = Timer(const Duration(milliseconds: 700), () {
|
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
|
||||||
context.read<DocumentSearchCubit>().suggest(query);
|
context.read<DocumentSearchCubit>().suggest(query);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
onSubmitted: (query) {
|
onSubmitted: (query) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
_debounceTimer?.cancel();
|
||||||
context.read<DocumentSearchCubit>().search(query);
|
context.read<DocumentSearchCubit>().search(query);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -110,9 +118,8 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSuggestionsView(DocumentSearchState state) {
|
Widget _buildSuggestionsView(DocumentSearchState state) {
|
||||||
final suggestions = state.suggestions
|
final suggestions =
|
||||||
.whereNot((element) => state.searchHistory.contains(element))
|
state.suggestions.whereNot((element) => state.searchHistory.contains(element)).toList();
|
||||||
.toList();
|
|
||||||
final historyMatches = state.searchHistory
|
final historyMatches = state.searchHistory
|
||||||
.where(
|
.where(
|
||||||
(element) => element.startsWith(query),
|
(element) => element.startsWith(query),
|
||||||
@@ -194,8 +201,7 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return ViewTypeSelectionWidget(
|
return ViewTypeSelectionWidget(
|
||||||
viewType: state.viewType,
|
viewType: state.viewType,
|
||||||
onChanged: (type) =>
|
onChanged: (type) => context.read<DocumentSearchCubit>().updateViewType(type),
|
||||||
context.read<DocumentSearchCubit>().updateViewType(type),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -229,6 +235,10 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
correspondents: state.correspondents,
|
||||||
|
documentTypes: state.documentTypes,
|
||||||
|
tags: state.tags,
|
||||||
|
storagePaths: state.storagePaths,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
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_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class RemoveHistoryEntryDialog extends StatelessWidget {
|
class RemoveHistoryEntryDialog extends StatelessWidget {
|
||||||
@@ -13,12 +14,10 @@ class RemoveHistoryEntryDialog extends StatelessWidget {
|
|||||||
content: Text(S.of(context)!.removeQueryFromSearchHistory),
|
content: Text(S.of(context)!.removeQueryFromSearchHistory),
|
||||||
actions: [
|
actions: [
|
||||||
const DialogCancelButton(),
|
const DialogCancelButton(),
|
||||||
TextButton(
|
DialogConfirmButton(
|
||||||
child: Text(S.of(context)!.remove),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
onPressed: () {
|
label: S.of(context)!.remove,
|
||||||
Navigator.pop(context, true);
|
)
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
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/bloc/paperless_server_information_cubit.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/server_information_cubit.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/delegate/customizable_sliver_persistent_header_delegate.dart';
|
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s;
|
||||||
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/settings/view/dialogs/account_settings_dialog.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/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class SliverSearchBar extends StatelessWidget {
|
class SliverSearchBar extends StatelessWidget {
|
||||||
@@ -23,12 +27,12 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
floating: floating,
|
floating: floating,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
delegate: CustomizableSliverPersistentHeaderDelegate(
|
delegate: CustomizableSliverPersistentHeaderDelegate(
|
||||||
minExtent: 56 + 8,
|
minExtent: kToolbarHeight,
|
||||||
maxExtent: 56 + 8,
|
maxExtent: kToolbarHeight,
|
||||||
child: Padding(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8.0),
|
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: SearchBar(
|
child: s.SearchBar(
|
||||||
height: 56,
|
height: kToolbarHeight,
|
||||||
supportingText: S.of(context)!.searchDocuments,
|
supportingText: S.of(context)!.searchDocuments,
|
||||||
onTap: () => showDocumentSearchPage(context),
|
onTap: () => showDocumentSearchPage(context),
|
||||||
leadingIcon: IconButton(
|
leadingIcon: IconButton(
|
||||||
@@ -36,18 +40,25 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
onPressed: Scaffold.of(context).openDrawer,
|
onPressed: Scaffold.of(context).openDrawer,
|
||||||
),
|
),
|
||||||
trailingIcon: IconButton(
|
trailingIcon: IconButton(
|
||||||
icon: BlocBuilder<PaperlessServerInformationCubit,
|
icon: GlobalSettingsBuilder(
|
||||||
PaperlessServerInformationState>(
|
builder: (context, settings) {
|
||||||
builder: (context, state) {
|
return ValueListenableBuilder(
|
||||||
return CircleAvatar(
|
valueListenable:
|
||||||
child: Text(state.information?.userInitials ?? ''),
|
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||||
|
builder: (context, box, _) {
|
||||||
|
final account = box.get(settings.currentLoggedInUser!)!;
|
||||||
|
return UserAvatar(userId: settings.currentLoggedInUser!, account: account);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const AccountSettingsDialog(),
|
builder: (_) => BlocProvider.value(
|
||||||
|
value: context.read<ServerInformationCubit>(),
|
||||||
|
child: const ManageAccountsPage(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,42 +5,26 @@ 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/repository/state/impl/correspondent_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
|
||||||
|
|
||||||
part 'document_upload_state.dart';
|
part 'document_upload_state.dart';
|
||||||
|
|
||||||
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||||
final PaperlessDocumentsApi _documentApi;
|
final PaperlessDocumentsApi _documentApi;
|
||||||
|
|
||||||
final LabelRepository<Tag> _tagRepository;
|
final LabelRepository _labelRepository;
|
||||||
final LabelRepository<Correspondent> _correspondentRepository;
|
|
||||||
final LabelRepository<DocumentType> _documentTypeRepository;
|
|
||||||
|
|
||||||
final List<StreamSubscription> _subs = [];
|
DocumentUploadCubit(this._labelRepository, this._documentApi)
|
||||||
|
: super(const DocumentUploadState()) {
|
||||||
DocumentUploadCubit({
|
_labelRepository.addListener(
|
||||||
required PaperlessDocumentsApi documentApi,
|
this,
|
||||||
required LabelRepository<Tag> tagRepository,
|
onChanged: (labels) {
|
||||||
required LabelRepository<Correspondent> correspondentRepository,
|
emit(state.copyWith(
|
||||||
required LabelRepository<DocumentType> documentTypeRepository,
|
correspondents: labels.correspondents,
|
||||||
}) : _documentApi = documentApi,
|
documentTypes: labels.documentTypes,
|
||||||
_tagRepository = tagRepository,
|
tags: labels.tags,
|
||||||
_correspondentRepository = correspondentRepository,
|
));
|
||||||
_documentTypeRepository = documentTypeRepository,
|
},
|
||||||
super(const DocumentUploadState()) {
|
);
|
||||||
_subs.add(_tagRepository.values.listen(
|
|
||||||
(tags) => emit(state.copyWith(tags: tags?.values)),
|
|
||||||
));
|
|
||||||
_subs.add(_correspondentRepository.values.listen(
|
|
||||||
(correspondents) =>
|
|
||||||
emit(state.copyWith(correspondents: correspondents?.values)),
|
|
||||||
));
|
|
||||||
_subs.add(_documentTypeRepository.values.listen(
|
|
||||||
(documentTypes) =>
|
|
||||||
emit(state.copyWith(documentTypes: documentTypes?.values)),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> upload(
|
Future<String?> upload(
|
||||||
@@ -65,9 +49,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
for (final sub in _subs) {
|
_labelRepository.removeListener(this);
|
||||||
await sub.cancel();
|
|
||||||
}
|
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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:form_builder_validators/form_builder_validators.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';
|
||||||
@@ -41,12 +41,10 @@ class DocumentUploadPreparationPage extends StatefulWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DocumentUploadPreparationPage> createState() =>
|
State<DocumentUploadPreparationPage> createState() => _DocumentUploadPreparationPageState();
|
||||||
_DocumentUploadPreparationPageState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentUploadPreparationPageState
|
class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparationPage> {
|
||||||
extends State<DocumentUploadPreparationPage> {
|
|
||||||
static const fkFileName = "filename";
|
static const fkFileName = "filename";
|
||||||
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
|
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
|
||||||
|
|
||||||
@@ -73,8 +71,7 @@ class _DocumentUploadPreparationPageState
|
|||||||
title: Text(S.of(context)!.prepareDocument),
|
title: Text(S.of(context)!.prepareDocument),
|
||||||
bottom: _isUploadLoading
|
bottom: _isUploadLoading
|
||||||
? const PreferredSize(
|
? const PreferredSize(
|
||||||
child: LinearProgressIndicator(),
|
child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0))
|
||||||
preferredSize: Size.fromHeight(4.0))
|
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
@@ -95,30 +92,30 @@ class _DocumentUploadPreparationPageState
|
|||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
name: DocumentModel.titleKey,
|
name: DocumentModel.titleKey,
|
||||||
initialValue:
|
initialValue: widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
|
||||||
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
|
validator: (value) {
|
||||||
validator: FormBuilderValidators.required(),
|
if (value?.trim().isEmpty ?? true) {
|
||||||
|
return S.of(context)!.thisFieldIsRequired;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: S.of(context)!.title,
|
labelText: S.of(context)!.title,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_formKey.currentState?.fields[DocumentModel.titleKey]
|
_formKey.currentState?.fields[DocumentModel.titleKey]?.didChange("");
|
||||||
?.didChange("");
|
|
||||||
if (_syncTitleAndFilename) {
|
if (_syncTitleAndFilename) {
|
||||||
_formKey.currentState?.fields[fkFileName]
|
_formKey.currentState?.fields[fkFileName]?.didChange("");
|
||||||
?.didChange("");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
errorText: _errors[DocumentModel.titleKey],
|
errorText: _errors[DocumentModel.titleKey],
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final String transformedValue =
|
final String transformedValue = _formatFilename(value ?? '');
|
||||||
_formatFilename(value ?? '');
|
|
||||||
if (_syncTitleAndFilename) {
|
if (_syncTitleAndFilename) {
|
||||||
_formKey.currentState?.fields[fkFileName]
|
_formKey.currentState?.fields[fkFileName]?.didChange(transformedValue);
|
||||||
?.didChange(transformedValue);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -133,12 +130,10 @@ class _DocumentUploadPreparationPageState
|
|||||||
suffixText: widget.fileExtension,
|
suffixText: widget.fileExtension,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
onPressed: () => _formKey.currentState?.fields[fkFileName]
|
onPressed: () => _formKey.currentState?.fields[fkFileName]?.didChange(''),
|
||||||
?.didChange(''),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
initialValue: widget.filename ??
|
initialValue: widget.filename ?? "scan_${fileNameDateFormat.format(_now)}",
|
||||||
"scan_${fileNameDateFormat.format(_now)}",
|
|
||||||
),
|
),
|
||||||
// Synchronize title and filename
|
// Synchronize title and filename
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
@@ -148,13 +143,10 @@ class _DocumentUploadPreparationPageState
|
|||||||
() => _syncTitleAndFilename = value,
|
() => _syncTitleAndFilename = value,
|
||||||
);
|
);
|
||||||
if (_syncTitleAndFilename) {
|
if (_syncTitleAndFilename) {
|
||||||
final String transformedValue = _formatFilename(_formKey
|
final String transformedValue = _formatFilename(
|
||||||
.currentState
|
_formKey.currentState?.fields[DocumentModel.titleKey]?.value as String);
|
||||||
?.fields[DocumentModel.titleKey]
|
|
||||||
?.value as String);
|
|
||||||
if (_syncTitleAndFilename) {
|
if (_syncTitleAndFilename) {
|
||||||
_formKey.currentState?.fields[fkFileName]
|
_formKey.currentState?.fields[fkFileName]?.didChange(transformedValue);
|
||||||
?.didChange(transformedValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -179,8 +171,7 @@ class _DocumentUploadPreparationPageState
|
|||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_formKey.currentState!
|
_formKey.currentState!.fields[DocumentModel.createdKey]
|
||||||
.fields[DocumentModel.createdKey]
|
|
||||||
?.didChange(null);
|
?.didChange(null);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -189,47 +180,44 @@ class _DocumentUploadPreparationPageState
|
|||||||
),
|
),
|
||||||
// Correspondent
|
// Correspondent
|
||||||
LabelFormField<Correspondent>(
|
LabelFormField<Correspondent>(
|
||||||
notAssignedSelectable: false,
|
showAnyAssignedOption: false,
|
||||||
formBuilderState: _formKey.currentState,
|
showNotAssignedOption: false,
|
||||||
labelCreationWidgetBuilder: (initialName) =>
|
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
|
||||||
RepositoryProvider(
|
value: context.read<LabelRepository>(),
|
||||||
create: (context) =>
|
|
||||||
context.read<LabelRepository<Correspondent>>(),
|
|
||||||
child: AddCorrespondentPage(initialName: initialName),
|
child: AddCorrespondentPage(initialName: initialName),
|
||||||
),
|
),
|
||||||
textFieldLabel: S.of(context)!.correspondent + " *",
|
addLabelText: S.of(context)!.addCorrespondent,
|
||||||
|
labelText: S.of(context)!.correspondent + " *",
|
||||||
name: DocumentModel.correspondentKey,
|
name: DocumentModel.correspondentKey,
|
||||||
labelOptions: state.correspondents,
|
options: state.correspondents,
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
),
|
),
|
||||||
// Document type
|
// Document type
|
||||||
LabelFormField<DocumentType>(
|
LabelFormField<DocumentType>(
|
||||||
notAssignedSelectable: false,
|
showAnyAssignedOption: false,
|
||||||
formBuilderState: _formKey.currentState,
|
showNotAssignedOption: false,
|
||||||
labelCreationWidgetBuilder: (initialName) =>
|
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
|
||||||
RepositoryProvider(
|
value: context.read<LabelRepository>(),
|
||||||
create: (context) =>
|
|
||||||
context.read<LabelRepository<DocumentType>>(),
|
|
||||||
child: AddDocumentTypePage(initialName: initialName),
|
child: AddDocumentTypePage(initialName: initialName),
|
||||||
),
|
),
|
||||||
textFieldLabel: S.of(context)!.documentType + " *",
|
addLabelText: S.of(context)!.addDocumentType,
|
||||||
|
labelText: S.of(context)!.documentType + " *",
|
||||||
name: DocumentModel.documentTypeKey,
|
name: DocumentModel.documentTypeKey,
|
||||||
labelOptions: state.documentTypes,
|
options: state.documentTypes,
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
),
|
),
|
||||||
TagFormField(
|
TagsFormField(
|
||||||
name: DocumentModel.tagsKey,
|
name: DocumentModel.tagsKey,
|
||||||
notAssignedSelectable: false,
|
allowCreation: true,
|
||||||
anyAssignedSelectable: false,
|
allowExclude: false,
|
||||||
excludeAllowed: false,
|
allowOnlySelection: true,
|
||||||
selectableOptions: state.tags,
|
options: state.tags,
|
||||||
//Label: "Tags" + " *",
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"* " + S.of(context)!.uploadInferValuesHint,
|
"* " + S.of(context)!.uploadInferValuesHint,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
SizedBox(height: 300),
|
const SizedBox(height: 300),
|
||||||
].padded(),
|
].padded(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -248,10 +236,9 @@ class _DocumentUploadPreparationPageState
|
|||||||
|
|
||||||
final createdAt = fv[DocumentModel.createdKey] as DateTime?;
|
final createdAt = fv[DocumentModel.createdKey] as DateTime?;
|
||||||
final title = fv[DocumentModel.titleKey] as String;
|
final title = fv[DocumentModel.titleKey] as String;
|
||||||
final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter;
|
final docType = fv[DocumentModel.documentTypeKey] as SetIdQueryParameter;
|
||||||
final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery;
|
final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery;
|
||||||
final correspondent =
|
final correspondent = fv[DocumentModel.correspondentKey] as SetIdQueryParameter;
|
||||||
fv[DocumentModel.correspondentKey] as IdQueryParameter;
|
|
||||||
|
|
||||||
final taskId = await cubit.upload(
|
final taskId = await cubit.upload(
|
||||||
widget.fileBytes,
|
widget.fileBytes,
|
||||||
@@ -262,7 +249,7 @@ class _DocumentUploadPreparationPageState
|
|||||||
title: title,
|
title: title,
|
||||||
documentType: docType.id,
|
documentType: docType.id,
|
||||||
correspondent: correspondent.id,
|
correspondent: correspondent.id,
|
||||||
tags: tags.ids,
|
tags: tags.include,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
);
|
);
|
||||||
showSnackBar(
|
showSnackBar(
|
||||||
@@ -279,8 +266,7 @@ class _DocumentUploadPreparationPageState
|
|||||||
setState(() => _errors = errors);
|
setState(() => _errors = errors);
|
||||||
} catch (unknownError, stackTrace) {
|
} catch (unknownError, stackTrace) {
|
||||||
debugPrint(unknownError.toString());
|
debugPrint(unknownError.toString());
|
||||||
showErrorMessage(
|
showErrorMessage(context, const PaperlessServerException.unknown(), stackTrace);
|
||||||
context, const PaperlessServerException.unknown(), stackTrace);
|
|
||||||
} finally {
|
} finally {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isUploadLoading = false;
|
_isUploadLoading = false;
|
||||||
|
|||||||
@@ -1,31 +1,68 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_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/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';
|
||||||
|
|
||||||
part 'documents_state.dart';
|
|
||||||
part 'documents_cubit.g.dart';
|
part 'documents_cubit.g.dart';
|
||||||
|
part 'documents_state.dart';
|
||||||
|
|
||||||
class DocumentsCubit extends HydratedCubit<DocumentsState>
|
class DocumentsCubit extends Cubit<DocumentsState> with DocumentPagingBlocMixin {
|
||||||
with DocumentPagingBlocMixin {
|
|
||||||
@override
|
@override
|
||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
|
final LabelRepository _labelRepository;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) {
|
final LocalUserAppState _userState;
|
||||||
notifier.subscribe(
|
|
||||||
|
DocumentsCubit(
|
||||||
|
this.api,
|
||||||
|
this.notifier,
|
||||||
|
this._labelRepository,
|
||||||
|
this._userState,
|
||||||
|
) : super(DocumentsState(
|
||||||
|
filter: _userState.currentDocumentFilter,
|
||||||
|
viewType: _userState.documentsPageViewType,
|
||||||
|
)) {
|
||||||
|
notifier.addListener(
|
||||||
this,
|
this,
|
||||||
onUpdated: replace,
|
onUpdated: (document) {
|
||||||
onDeleted: remove,
|
replace(document);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selection: state.selection.map((e) => e.id == document.id ? document : e).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onDeleted: (document) {
|
||||||
|
remove(document);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selection: state.selection.where((e) => e.id != document.id).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
_labelRepository.addListener(
|
||||||
|
this,
|
||||||
|
onChanged: (labels) => emit(
|
||||||
|
state.copyWith(
|
||||||
|
correspondents: labels.correspondents,
|
||||||
|
documentTypes: labels.documentTypes,
|
||||||
|
storagePaths: labels.storagePaths,
|
||||||
|
tags: labels.tags,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,28 +77,12 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
await reload();
|
await reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> bulkEditTags(
|
|
||||||
Iterable<DocumentModel> documents, {
|
|
||||||
Iterable<int> addTags = const [],
|
|
||||||
Iterable<int> removeTags = const [],
|
|
||||||
}) async {
|
|
||||||
debugPrint("[DocumentsCubit] bulkEditTags");
|
|
||||||
await api.bulkAction(BulkModifyTagsAction(
|
|
||||||
documents.map((doc) => doc.id),
|
|
||||||
addTags: addTags,
|
|
||||||
removeTags: removeTags,
|
|
||||||
));
|
|
||||||
await reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleDocumentSelection(DocumentModel model) {
|
void toggleDocumentSelection(DocumentModel model) {
|
||||||
debugPrint("[DocumentsCubit] toggleSelection");
|
debugPrint("[DocumentsCubit] toggleSelection");
|
||||||
if (state.selectedIds.contains(model.id)) {
|
if (state.selectedIds.contains(model.id)) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
selection: state.selection
|
selection: state.selection.where((element) => element.id != model.id).toList(),
|
||||||
.where((element) => element.id != model.id)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -84,23 +105,22 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
DocumentsState? fromJson(Map<String, dynamic> json) {
|
|
||||||
return DocumentsState.fromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic>? toJson(DocumentsState state) {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
notifier.unsubscribe(this);
|
notifier.removeListener(this);
|
||||||
|
_labelRepository.removeListener(this);
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setViewType(ViewType viewType) {
|
void setViewType(ViewType viewType) {
|
||||||
emit(state.copyWith(viewType: viewType));
|
emit(state.copyWith(viewType: viewType));
|
||||||
|
_userState.documentsPageViewType = viewType;
|
||||||
|
_userState.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onFilterUpdated(DocumentFilter filter) async {
|
||||||
|
_userState.currentDocumentFilter = filter;
|
||||||
|
await _userState.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,18 @@ part of 'documents_cubit.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class DocumentsState extends DocumentPagingState {
|
class DocumentsState extends DocumentPagingState {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
final List<DocumentModel> selection;
|
final List<DocumentModel> selection;
|
||||||
|
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final Map<int, Correspondent> correspondents;
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final Map<int, DocumentType> documentTypes;
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final Map<int, Tag> tags;
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final Map<int, StoragePath> storagePaths;
|
||||||
|
|
||||||
final ViewType viewType;
|
final ViewType viewType;
|
||||||
|
|
||||||
const DocumentsState({
|
const DocumentsState({
|
||||||
@@ -14,6 +23,10 @@ class DocumentsState extends DocumentPagingState {
|
|||||||
super.filter = const DocumentFilter(),
|
super.filter = const DocumentFilter(),
|
||||||
super.hasLoaded = false,
|
super.hasLoaded = false,
|
||||||
super.isLoading = false,
|
super.isLoading = false,
|
||||||
|
this.correspondents = const {},
|
||||||
|
this.documentTypes = const {},
|
||||||
|
this.tags = const {},
|
||||||
|
this.storagePaths = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
List<int> get selectedIds => selection.map((e) => e.id).toList();
|
List<int> get selectedIds => selection.map((e) => e.id).toList();
|
||||||
@@ -25,6 +38,10 @@ class DocumentsState extends DocumentPagingState {
|
|||||||
DocumentFilter? filter,
|
DocumentFilter? filter,
|
||||||
List<DocumentModel>? selection,
|
List<DocumentModel>? selection,
|
||||||
ViewType? viewType,
|
ViewType? viewType,
|
||||||
|
Map<int, Correspondent>? correspondents,
|
||||||
|
Map<int, DocumentType>? documentTypes,
|
||||||
|
Map<int, Tag>? tags,
|
||||||
|
Map<int, StoragePath>? storagePaths,
|
||||||
}) {
|
}) {
|
||||||
return DocumentsState(
|
return DocumentsState(
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
@@ -33,18 +50,21 @@ class DocumentsState extends DocumentPagingState {
|
|||||||
filter: filter ?? this.filter,
|
filter: filter ?? this.filter,
|
||||||
selection: selection ?? this.selection,
|
selection: selection ?? this.selection,
|
||||||
viewType: viewType ?? this.viewType,
|
viewType: viewType ?? this.viewType,
|
||||||
|
correspondents: correspondents ?? this.correspondents,
|
||||||
|
documentTypes: documentTypes ?? this.documentTypes,
|
||||||
|
tags: tags ?? this.tags,
|
||||||
|
storagePaths: storagePaths ?? this.storagePaths,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DocumentsStateFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
selection,
|
selection,
|
||||||
viewType,
|
viewType,
|
||||||
|
correspondents,
|
||||||
|
documentTypes,
|
||||||
|
tags,
|
||||||
|
storagePaths,
|
||||||
...super.props,
|
...super.props,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -62,4 +82,9 @@ class DocumentsState extends DocumentPagingState {
|
|||||||
value: value,
|
value: value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DocumentsStateFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import 'package:badges/badges.dart' as b;
|
import 'package:badges/badges.dart' as b;
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/colored_tab_bar.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_details/view/pages/document_details_page.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/search/document_filter_panel.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/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/providers/labels_bloc_provider.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/add_saved_view_page.dart';
|
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
|
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
|
||||||
@@ -44,12 +42,9 @@ class DocumentsPage extends StatefulWidget {
|
|||||||
State<DocumentsPage> createState() => _DocumentsPageState();
|
State<DocumentsPage> createState() => _DocumentsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentsPageState extends State<DocumentsPage>
|
class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProviderStateMixin {
|
||||||
with SingleTickerProviderStateMixin {
|
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
|
||||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
final SliverOverlapAbsorberHandle tabBarHandle = SliverOverlapAbsorberHandle();
|
||||||
SliverOverlapAbsorberHandle();
|
|
||||||
final SliverOverlapAbsorberHandle tabBarHandle =
|
|
||||||
SliverOverlapAbsorberHandle();
|
|
||||||
late final TabController _tabController;
|
late final TabController _tabController;
|
||||||
|
|
||||||
int _currentTab = 0;
|
int _currentTab = 0;
|
||||||
@@ -86,8 +81,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<TaskStatusCubit, TaskStatusState>(
|
return BlocListener<TaskStatusCubit, TaskStatusState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) => !previous.isSuccess && current.isSuccess,
|
||||||
!previous.isSuccess && current.isSuccess,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
showSnackBar(
|
showSnackBar(
|
||||||
context,
|
context,
|
||||||
@@ -104,8 +98,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
},
|
},
|
||||||
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous != ConnectivityState.connected &&
|
previous != ConnectivityState.connected && current == ConnectivityState.connected,
|
||||||
current == ConnectivityState.connected,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
try {
|
try {
|
||||||
context.read<DocumentsCubit>().reload();
|
context.read<DocumentsCubit>().reload();
|
||||||
@@ -115,42 +108,45 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
},
|
},
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
|
top: context.read<DocumentsCubit>().state.selection.isEmpty,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||||
return b.Badge(
|
final show = state.selection.isEmpty;
|
||||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
return AnimatedScale(
|
||||||
showBadge: appliedFiltersCount > 0,
|
scale: show ? 1 : 0,
|
||||||
badgeContent: Text(
|
duration: const Duration(milliseconds: 200),
|
||||||
'$appliedFiltersCount',
|
curve: Curves.easeIn,
|
||||||
style: const TextStyle(
|
child: b.Badge(
|
||||||
color: Colors.white,
|
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||||
|
showBadge: appliedFiltersCount > 0,
|
||||||
|
badgeContent: Text(
|
||||||
|
'$appliedFiltersCount',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
animationType: b.BadgeAnimationType.fade,
|
||||||
|
badgeColor: Colors.red,
|
||||||
|
child: _currentTab == 0
|
||||||
|
? FloatingActionButton(
|
||||||
|
child: const Icon(Icons.filter_alt_outlined),
|
||||||
|
onPressed: _openDocumentFilter,
|
||||||
|
)
|
||||||
|
: FloatingActionButton(
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
onPressed: () => _onCreateSavedView(state.filter),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
animationType: b.BadgeAnimationType.fade,
|
|
||||||
badgeColor: Colors.red,
|
|
||||||
child: _currentTab == 0
|
|
||||||
? FloatingActionButton(
|
|
||||||
child: const Icon(Icons.filter_alt_outlined),
|
|
||||||
onPressed: _openDocumentFilter,
|
|
||||||
)
|
|
||||||
: FloatingActionButton(
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
onPressed: () => _onCreateSavedView(state.filter),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
body: WillPopScope(
|
body: WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
if (context
|
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.state
|
|
||||||
.selection
|
|
||||||
.isNotEmpty) {
|
|
||||||
context.read<DocumentsCubit>().resetSelection();
|
context.read<DocumentsCubit>().resetSelection();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -167,7 +163,8 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
if (state.selection.isNotEmpty) {
|
if (state.selection.isNotEmpty) {
|
||||||
// Show selection app bar when selection mode is active
|
// Show selection app bar when selection mode is active
|
||||||
return DocumentSelectionSliverAppBar(
|
return DocumentSelectionSliverAppBar(
|
||||||
state: state);
|
state: state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return const SliverSearchBar(floating: true);
|
return const SliverSearchBar(floating: true);
|
||||||
},
|
},
|
||||||
@@ -184,8 +181,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
}
|
}
|
||||||
return SliverPersistentHeader(
|
return SliverPersistentHeader(
|
||||||
pinned: true,
|
pinned: true,
|
||||||
delegate:
|
delegate: CustomizableSliverPersistentHeaderDelegate(
|
||||||
CustomizableSliverPersistentHeaderDelegate(
|
|
||||||
minExtent: kTextTabBarHeight,
|
minExtent: kTextTabBarHeight,
|
||||||
maxExtent: kTextTabBarHeight,
|
maxExtent: kTextTabBarHeight,
|
||||||
child: ColoredTabBar(
|
child: ColoredTabBar(
|
||||||
@@ -209,22 +205,15 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
if (metrics.maxScrollExtent == 0) {
|
if (metrics.maxScrollExtent == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final desiredTab =
|
final desiredTab = (metrics.pixels / metrics.maxScrollExtent).round();
|
||||||
(metrics.pixels / metrics.maxScrollExtent)
|
if (metrics.axis == Axis.horizontal && _currentTab != desiredTab) {
|
||||||
.round();
|
|
||||||
if (metrics.axis == Axis.horizontal &&
|
|
||||||
_currentTab != desiredTab) {
|
|
||||||
setState(() => _currentTab = desiredTab);
|
setState(() => _currentTab = desiredTab);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
physics: context
|
physics: context.watch<DocumentsCubit>().state.selection.isNotEmpty
|
||||||
.watch<DocumentsCubit>()
|
|
||||||
.state
|
|
||||||
.selection
|
|
||||||
.isNotEmpty
|
|
||||||
? const NeverScrollableScrollPhysics()
|
? const NeverScrollableScrollPhysics()
|
||||||
: null,
|
: null,
|
||||||
children: [
|
children: [
|
||||||
@@ -292,25 +281,20 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
|
|
||||||
final currState = context.read<DocumentsCubit>().state;
|
final currState = context.read<DocumentsCubit>().state;
|
||||||
final max = notification.metrics.maxScrollExtent;
|
final max = notification.metrics.maxScrollExtent;
|
||||||
if (max == 0 ||
|
if (max == 0 || _currentTab != 0 || currState.isLoading || currState.isLastPageLoaded) {
|
||||||
_currentTab != 0 ||
|
return false;
|
||||||
currState.isLoading ||
|
|
||||||
currState.isLastPageLoaded) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final offset = notification.metrics.pixels;
|
final offset = notification.metrics.pixels;
|
||||||
if (offset >= max * 0.7) {
|
if (offset >= max * 0.7) {
|
||||||
context
|
context.read<DocumentsCubit>().loadMore().onError<PaperlessServerException>(
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.loadMore()
|
|
||||||
.onError<PaperlessServerException>(
|
|
||||||
(error, stackTrace) => showErrorMessage(
|
(error, stackTrace) => showErrorMessage(
|
||||||
context,
|
context,
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -338,8 +322,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
return SliverAdaptiveDocumentsView(
|
return SliverAdaptiveDocumentsView(
|
||||||
viewType: state.viewType,
|
viewType: state.viewType,
|
||||||
onTap: _openDetails,
|
onTap: _openDetails,
|
||||||
onSelected:
|
onSelected: context.read<DocumentsCubit>().toggleDocumentSelection,
|
||||||
context.read<DocumentsCubit>().toggleDocumentSelection,
|
|
||||||
hasInternetConnection: connectivityState.isConnected,
|
hasInternetConnection: connectivityState.isConnected,
|
||||||
onTagSelected: _addTagToFilter,
|
onTagSelected: _addTagToFilter,
|
||||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
onCorrespondentSelected: _addCorrespondentToFilter,
|
||||||
@@ -350,6 +333,10 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
isLabelClickable: true,
|
isLabelClickable: true,
|
||||||
isLoading: state.isLoading,
|
isLoading: state.isLoading,
|
||||||
selectedDocumentIds: state.selectedIds,
|
selectedDocumentIds: state.selectedIds,
|
||||||
|
correspondents: state.correspondents,
|
||||||
|
documentTypes: state.documentTypes,
|
||||||
|
tags: state.tags,
|
||||||
|
storagePaths: state.storagePaths,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -361,53 +348,38 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
|
|
||||||
Widget _buildViewActions() {
|
Widget _buildViewActions() {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Row(
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
builder: (context, state) {
|
||||||
children: [
|
return Row(
|
||||||
const SortDocumentsButton(),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
children: [
|
||||||
builder: (context, state) {
|
SortDocumentsButton(
|
||||||
return ViewTypeSelectionWidget(
|
enabled: state.selection.isEmpty,
|
||||||
|
),
|
||||||
|
ViewTypeSelectionWidget(
|
||||||
viewType: state.viewType,
|
viewType: state.viewType,
|
||||||
onChanged: context.read<DocumentsCubit>().setViewType,
|
onChanged: context.read<DocumentsCubit>().setViewType,
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
)
|
);
|
||||||
],
|
},
|
||||||
).paddedSymmetrically(horizontal: 8, vertical: 4),
|
).paddedSymmetrically(horizontal: 8, vertical: 4),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDelete(DocumentsState documentsState) async {
|
|
||||||
final shouldDelete = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
BulkDeleteConfirmationDialog(state: documentsState),
|
|
||||||
) ??
|
|
||||||
false;
|
|
||||||
if (shouldDelete) {
|
|
||||||
try {
|
|
||||||
await context
|
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.bulkDelete(documentsState.selection);
|
|
||||||
showSnackBar(
|
|
||||||
context,
|
|
||||||
S.of(context)!.documentsSuccessfullyDeleted,
|
|
||||||
);
|
|
||||||
context.read<DocumentsCubit>().resetSelection();
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onCreateSavedView(DocumentFilter filter) async {
|
void _onCreateSavedView(DocumentFilter filter) async {
|
||||||
final newView = await Navigator.of(context).push<SavedView?>(
|
final newView = await Navigator.of(context).push<SavedView?>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => LabelsBlocProvider(
|
builder: (context) => BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
child: AddSavedViewPage(
|
builder: (context, state) {
|
||||||
currentFilter: filter,
|
return AddSavedViewPage(
|
||||||
),
|
currentFilter: filter,
|
||||||
|
correspondents: state.correspondents,
|
||||||
|
documentTypes: state.documentTypes,
|
||||||
|
storagePaths: state.storagePaths,
|
||||||
|
tags: state.tags,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -441,12 +413,18 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
snapSizes: const [0.9, 1],
|
snapSizes: const [0.9, 1],
|
||||||
initialChildSize: .9,
|
initialChildSize: .9,
|
||||||
maxChildSize: 1,
|
maxChildSize: 1,
|
||||||
builder: (context, controller) => LabelsBlocProvider(
|
builder: (context, controller) => BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
child: DocumentFilterPanel(
|
builder: (context, state) {
|
||||||
initialFilter: context.read<DocumentsCubit>().state.filter,
|
return DocumentFilterPanel(
|
||||||
scrollController: controller,
|
initialFilter: context.read<DocumentsCubit>().state.filter,
|
||||||
draggableSheetController: draggableSheetController,
|
scrollController: controller,
|
||||||
),
|
draggableSheetController: draggableSheetController,
|
||||||
|
correspondents: state.correspondents,
|
||||||
|
documentTypes: state.documentTypes,
|
||||||
|
storagePaths: state.storagePaths,
|
||||||
|
tags: state.tags,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -456,9 +434,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
if (filterIntent.shouldReset) {
|
if (filterIntent.shouldReset) {
|
||||||
await context.read<DocumentsCubit>().resetFilter();
|
await context.read<DocumentsCubit>().resetFilter();
|
||||||
} else {
|
} else {
|
||||||
await context
|
await context.read<DocumentsCubit>().updateFilter(filter: filterIntent.filter!);
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.updateFilter(filter: filterIntent.filter!);
|
|
||||||
}
|
}
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
@@ -478,20 +454,21 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
|
|
||||||
void _addTagToFilter(int tagId) {
|
void _addTagToFilter(int tagId) {
|
||||||
try {
|
try {
|
||||||
final tagsQuery =
|
final tagsQuery = context.read<DocumentsCubit>().state.filter.tags is IdsTagsQuery
|
||||||
context.read<DocumentsCubit>().state.filter.tags is IdsTagsQuery
|
? context.read<DocumentsCubit>().state.filter.tags as IdsTagsQuery
|
||||||
? context.read<DocumentsCubit>().state.filter.tags as IdsTagsQuery
|
: const IdsTagsQuery();
|
||||||
: const IdsTagsQuery();
|
if (tagsQuery.include.contains(tagId)) {
|
||||||
if (tagsQuery.includedIds.contains(tagId)) {
|
|
||||||
context.read<DocumentsCubit>().updateCurrentFilter(
|
context.read<DocumentsCubit>().updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(
|
(filter) => filter.copyWith(
|
||||||
tags: tagsQuery.withIdsRemoved([tagId]),
|
tags: tagsQuery.copyWith(
|
||||||
|
include: tagsQuery.include.whereNot((id) => id == tagId).toList(),
|
||||||
|
exclude: tagsQuery.exclude.whereNot((id) => id == tagId).toList()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.read<DocumentsCubit>().updateCurrentFilter(
|
context.read<DocumentsCubit>().updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(
|
(filter) => filter.copyWith(
|
||||||
tags: tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tagId)]),
|
tags: tagsQuery.copyWith(include: [...tagsQuery.include, tagId]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -503,16 +480,17 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
void _addCorrespondentToFilter(int? correspondentId) {
|
void _addCorrespondentToFilter(int? correspondentId) {
|
||||||
final cubit = context.read<DocumentsCubit>();
|
final cubit = context.read<DocumentsCubit>();
|
||||||
try {
|
try {
|
||||||
if (cubit.state.filter.correspondent.id == correspondentId) {
|
final correspondent = cubit.state.filter.correspondent;
|
||||||
cubit.updateCurrentFilter(
|
if (correspondent is SetIdQueryParameter) {
|
||||||
(filter) =>
|
if (correspondentId == null || correspondent.id == correspondentId) {
|
||||||
filter.copyWith(correspondent: const IdQueryParameter.unset()),
|
cubit.updateCurrentFilter(
|
||||||
);
|
(filter) => filter.copyWith(correspondent: const IdQueryParameter.unset()),
|
||||||
} else {
|
);
|
||||||
cubit.updateCurrentFilter(
|
} else {
|
||||||
(filter) => filter.copyWith(
|
cubit.updateCurrentFilter(
|
||||||
correspondent: IdQueryParameter.fromId(correspondentId)),
|
(filter) => filter.copyWith(correspondent: IdQueryParameter.fromId(correspondentId)),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
@@ -522,16 +500,17 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
void _addDocumentTypeToFilter(int? documentTypeId) {
|
void _addDocumentTypeToFilter(int? documentTypeId) {
|
||||||
final cubit = context.read<DocumentsCubit>();
|
final cubit = context.read<DocumentsCubit>();
|
||||||
try {
|
try {
|
||||||
if (cubit.state.filter.documentType.id == documentTypeId) {
|
final documentType = cubit.state.filter.documentType;
|
||||||
cubit.updateCurrentFilter(
|
if (documentType is SetIdQueryParameter) {
|
||||||
(filter) =>
|
if (documentTypeId == null || documentType.id == documentTypeId) {
|
||||||
filter.copyWith(documentType: const IdQueryParameter.unset()),
|
cubit.updateCurrentFilter(
|
||||||
);
|
(filter) => filter.copyWith(documentType: const IdQueryParameter.unset()),
|
||||||
} else {
|
);
|
||||||
cubit.updateCurrentFilter(
|
} else {
|
||||||
(filter) => filter.copyWith(
|
cubit.updateCurrentFilter(
|
||||||
documentType: IdQueryParameter.fromId(documentTypeId)),
|
(filter) => filter.copyWith(documentType: IdQueryParameter.fromId(documentTypeId)),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
@@ -541,16 +520,17 @@ class _DocumentsPageState extends State<DocumentsPage>
|
|||||||
void _addStoragePathToFilter(int? pathId) {
|
void _addStoragePathToFilter(int? pathId) {
|
||||||
final cubit = context.read<DocumentsCubit>();
|
final cubit = context.read<DocumentsCubit>();
|
||||||
try {
|
try {
|
||||||
if (cubit.state.filter.correspondent.id == pathId) {
|
final path = cubit.state.filter.documentType;
|
||||||
cubit.updateCurrentFilter(
|
if (path is SetIdQueryParameter) {
|
||||||
(filter) =>
|
if (pathId == null || path.id == pathId) {
|
||||||
filter.copyWith(storagePath: const IdQueryParameter.unset()),
|
cubit.updateCurrentFilter(
|
||||||
);
|
(filter) => filter.copyWith(storagePath: const IdQueryParameter.unset()),
|
||||||
} else {
|
);
|
||||||
cubit.updateCurrentFilter(
|
} else {
|
||||||
(filter) =>
|
cubit.updateCurrentFilter(
|
||||||
filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)),
|
(filter) => filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
|
||||||
@@ -25,7 +24,13 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
|
|||||||
final void Function(int? id)? onDocumentTypeSelected;
|
final void Function(int? id)? onDocumentTypeSelected;
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
final void Function(int? id)? onStoragePathSelected;
|
||||||
|
|
||||||
bool get showLoadingPlaceholder => (!hasLoaded && isLoading);
|
final Map<int, Correspondent> correspondents;
|
||||||
|
final Map<int, DocumentType> documentTypes;
|
||||||
|
final Map<int, Tag> tags;
|
||||||
|
final Map<int, StoragePath> storagePaths;
|
||||||
|
|
||||||
|
bool get showLoadingPlaceholder => !hasLoaded && isLoading;
|
||||||
|
|
||||||
const AdaptiveDocumentsView({
|
const AdaptiveDocumentsView({
|
||||||
super.key,
|
super.key,
|
||||||
this.selectedDocumentIds = const [],
|
this.selectedDocumentIds = const [],
|
||||||
@@ -42,6 +47,10 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
|
|||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.hasLoaded,
|
required this.hasLoaded,
|
||||||
this.enableHeroAnimation = true,
|
this.enableHeroAnimation = true,
|
||||||
|
required this.correspondents,
|
||||||
|
required this.documentTypes,
|
||||||
|
required this.tags,
|
||||||
|
required this.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
AdaptiveDocumentsView.fromPagedState(
|
AdaptiveDocumentsView.fromPagedState(
|
||||||
@@ -58,6 +67,10 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
|
|||||||
required this.hasInternetConnection,
|
required this.hasInternetConnection,
|
||||||
this.viewType = ViewType.list,
|
this.viewType = ViewType.list,
|
||||||
this.selectedDocumentIds = const [],
|
this.selectedDocumentIds = const [],
|
||||||
|
required this.correspondents,
|
||||||
|
required this.documentTypes,
|
||||||
|
required this.tags,
|
||||||
|
required this.storagePaths,
|
||||||
}) : documents = state.documents,
|
}) : documents = state.documents,
|
||||||
isLoading = state.isLoading,
|
isLoading = state.isLoading,
|
||||||
hasLoaded = state.hasLoaded;
|
hasLoaded = state.hasLoaded;
|
||||||
@@ -80,6 +93,10 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
super.enableHeroAnimation,
|
super.enableHeroAnimation,
|
||||||
required super.isLoading,
|
required super.isLoading,
|
||||||
required super.hasLoaded,
|
required super.hasLoaded,
|
||||||
|
required super.correspondents,
|
||||||
|
required super.documentTypes,
|
||||||
|
required super.tags,
|
||||||
|
required super.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -96,27 +113,29 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
|
|
||||||
Widget _buildListView() {
|
Widget _buildListView() {
|
||||||
if (showLoadingPlaceholder) {
|
if (showLoadingPlaceholder) {
|
||||||
return DocumentsListLoadingWidget.sliver();
|
return const DocumentsListLoadingWidget.sliver();
|
||||||
}
|
}
|
||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
childCount: documents.length,
|
childCount: documents.length,
|
||||||
(context, index) {
|
(context, index) {
|
||||||
final document = documents.elementAt(index);
|
final document = documents.elementAt(index);
|
||||||
return LabelRepositoriesProvider(
|
return DocumentListItem(
|
||||||
child: DocumentListItem(
|
isLabelClickable: isLabelClickable,
|
||||||
isLabelClickable: isLabelClickable,
|
document: document,
|
||||||
document: document,
|
onTap: onTap,
|
||||||
onTap: onTap,
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
isSelected: selectedDocumentIds.contains(document.id),
|
onSelected: onSelected,
|
||||||
onSelected: onSelected,
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
onTagSelected: onTagSelected,
|
||||||
onTagSelected: onTagSelected,
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
enableHeroAnimation: enableHeroAnimation,
|
correspondents: correspondents,
|
||||||
),
|
documentTypes: documentTypes,
|
||||||
|
storagePaths: storagePaths,
|
||||||
|
tags: tags,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -126,28 +145,30 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
Widget _buildFullView(BuildContext context) {
|
Widget _buildFullView(BuildContext context) {
|
||||||
if (showLoadingPlaceholder) {
|
if (showLoadingPlaceholder) {
|
||||||
//TODO: Build detailed loading animation
|
//TODO: Build detailed loading animation
|
||||||
return DocumentsListLoadingWidget.sliver();
|
return const DocumentsListLoadingWidget.sliver();
|
||||||
}
|
}
|
||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
childCount: documents.length,
|
childCount: documents.length,
|
||||||
(context, index) {
|
(context, index) {
|
||||||
final document = documents.elementAt(index);
|
final document = documents.elementAt(index);
|
||||||
return LabelRepositoriesProvider(
|
return DocumentDetailedItem(
|
||||||
child: DocumentDetailedItem(
|
isLabelClickable: isLabelClickable,
|
||||||
isLabelClickable: isLabelClickable,
|
document: document,
|
||||||
document: document,
|
onTap: onTap,
|
||||||
onTap: onTap,
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
isSelected: selectedDocumentIds.contains(document.id),
|
onSelected: onSelected,
|
||||||
onSelected: onSelected,
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
onTagSelected: onTagSelected,
|
||||||
onTagSelected: onTagSelected,
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
enableHeroAnimation: enableHeroAnimation,
|
highlights: document.searchHit?.highlights,
|
||||||
highlights: document.searchHit?.highlights,
|
correspondents: correspondents,
|
||||||
),
|
documentTypes: documentTypes,
|
||||||
|
storagePaths: storagePaths,
|
||||||
|
tags: tags,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -180,6 +201,10 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
enableHeroAnimation: enableHeroAnimation,
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
correspondents: correspondents,
|
||||||
|
documentTypes: documentTypes,
|
||||||
|
storagePaths: storagePaths,
|
||||||
|
tags: tags,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -205,6 +230,10 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
super.selectedDocumentIds,
|
super.selectedDocumentIds,
|
||||||
super.viewType,
|
super.viewType,
|
||||||
super.enableHeroAnimation = true,
|
super.enableHeroAnimation = true,
|
||||||
|
required super.correspondents,
|
||||||
|
required super.documentTypes,
|
||||||
|
required super.tags,
|
||||||
|
required super.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -231,20 +260,22 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
itemCount: documents.length,
|
itemCount: documents.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final document = documents.elementAt(index);
|
final document = documents.elementAt(index);
|
||||||
return LabelRepositoriesProvider(
|
return DocumentListItem(
|
||||||
child: DocumentListItem(
|
isLabelClickable: isLabelClickable,
|
||||||
isLabelClickable: isLabelClickable,
|
document: document,
|
||||||
document: document,
|
onTap: onTap,
|
||||||
onTap: onTap,
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
isSelected: selectedDocumentIds.contains(document.id),
|
onSelected: onSelected,
|
||||||
onSelected: onSelected,
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
onTagSelected: onTagSelected,
|
||||||
onTagSelected: onTagSelected,
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
enableHeroAnimation: enableHeroAnimation,
|
correspondents: correspondents,
|
||||||
),
|
documentTypes: documentTypes,
|
||||||
|
storagePaths: storagePaths,
|
||||||
|
tags: tags,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -252,7 +283,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
|
|
||||||
Widget _buildFullView() {
|
Widget _buildFullView() {
|
||||||
if (showLoadingPlaceholder) {
|
if (showLoadingPlaceholder) {
|
||||||
return DocumentsListLoadingWidget();
|
return const DocumentsListLoadingWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
@@ -263,20 +294,22 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
itemCount: documents.length,
|
itemCount: documents.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final document = documents.elementAt(index);
|
final document = documents.elementAt(index);
|
||||||
return LabelRepositoriesProvider(
|
return DocumentDetailedItem(
|
||||||
child: DocumentDetailedItem(
|
isLabelClickable: isLabelClickable,
|
||||||
isLabelClickable: isLabelClickable,
|
document: document,
|
||||||
document: document,
|
onTap: onTap,
|
||||||
onTap: onTap,
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
isSelected: selectedDocumentIds.contains(document.id),
|
onSelected: onSelected,
|
||||||
onSelected: onSelected,
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
onTagSelected: onTagSelected,
|
||||||
onTagSelected: onTagSelected,
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
enableHeroAnimation: enableHeroAnimation,
|
correspondents: correspondents,
|
||||||
),
|
documentTypes: documentTypes,
|
||||||
|
storagePaths: storagePaths,
|
||||||
|
tags: tags,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -284,7 +317,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
|
|
||||||
Widget _buildGridView() {
|
Widget _buildGridView() {
|
||||||
if (showLoadingPlaceholder) {
|
if (showLoadingPlaceholder) {
|
||||||
return DocumentGridLoadingWidget();
|
return const DocumentGridLoadingWidget();
|
||||||
}
|
}
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
@@ -311,6 +344,10 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
|||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
enableHeroAnimation: enableHeroAnimation,
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
correspondents: correspondents,
|
||||||
|
documentTypes: documentTypes,
|
||||||
|
storagePaths: storagePaths,
|
||||||
|
tags: tags,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/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';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
||||||
@@ -30,19 +32,10 @@ class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.pop(context, false),
|
DialogConfirmButton(
|
||||||
child: Text(S.of(context)!.cancel),
|
label: S.of(context)!.delete,
|
||||||
),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
TextButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
foregroundColor:
|
|
||||||
MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
child: Text(S.of(context)!.delete),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.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';
|
||||||
@@ -8,7 +9,6 @@ import 'package:paperless_mobile/features/documents/view/widgets/items/document_
|
|||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
|
||||||
|
|
||||||
class DocumentDetailedItem extends DocumentItem {
|
class DocumentDetailedItem extends DocumentItem {
|
||||||
final String? highlights;
|
final String? highlights;
|
||||||
@@ -26,6 +26,10 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
super.onStoragePathSelected,
|
super.onStoragePathSelected,
|
||||||
super.onTagSelected,
|
super.onTagSelected,
|
||||||
super.onTap,
|
super.onTap,
|
||||||
|
required super.tags,
|
||||||
|
required super.correspondents,
|
||||||
|
required super.documentTypes,
|
||||||
|
required super.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -40,10 +44,10 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
padding.bottom -
|
padding.bottom -
|
||||||
kBottomNavigationBarHeight -
|
kBottomNavigationBarHeight -
|
||||||
kToolbarHeight;
|
kToolbarHeight;
|
||||||
final maxHeight = highlights != null
|
final maxHeight =
|
||||||
? min(600.0, availableHeight)
|
highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight);
|
||||||
: min(500.0, availableHeight);
|
|
||||||
return Card(
|
return Card(
|
||||||
|
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -112,7 +116,7 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
correspondentId: document.correspondent,
|
correspondent: correspondents[document.correspondent],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddedLTRB(8, 0, 8, 4),
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
@@ -127,13 +131,13 @@ class DocumentDetailedItem extends DocumentItem {
|
|||||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
documentTypeId: document.documentType,
|
documentType: documentTypes[document.documentType],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddedLTRB(8, 0, 8, 4),
|
).paddedLTRB(8, 0, 8, 4),
|
||||||
TagsWidget(
|
TagsWidget(
|
||||||
isMultiLine: false,
|
isMultiLine: false,
|
||||||
tagIds: document.tags,
|
tags: document.tags.map((e) => tags[e]!).toList(),
|
||||||
).padded(),
|
).padded(),
|
||||||
if (highlights != null)
|
if (highlights != null)
|
||||||
Html(
|
Html(
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ class DocumentGridItem extends DocumentItem {
|
|||||||
super.onTagSelected,
|
super.onTagSelected,
|
||||||
super.onTap,
|
super.onTap,
|
||||||
required super.enableHeroAnimation,
|
required super.enableHeroAnimation,
|
||||||
|
required super.tags,
|
||||||
|
required super.correspondents,
|
||||||
|
required super.documentTypes,
|
||||||
|
required super.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -54,10 +58,10 @@ class DocumentGridItem extends DocumentItem {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CorrespondentWidget(
|
CorrespondentWidget(
|
||||||
correspondentId: document.correspondent,
|
correspondent: correspondents[document.correspondent],
|
||||||
),
|
),
|
||||||
DocumentTypeWidget(
|
DocumentTypeWidget(
|
||||||
documentTypeId: document.documentType,
|
documentType: documentTypes[document.documentType],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
document.title,
|
document.title,
|
||||||
@@ -67,7 +71,7 @@ class DocumentGridItem extends DocumentItem {
|
|||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
TagsWidget(
|
TagsWidget(
|
||||||
tagIds: document.tags,
|
tags: document.tags.map((e) => tags[e]!).toList(),
|
||||||
isMultiLine: false,
|
isMultiLine: false,
|
||||||
onTagSelected: onTagSelected,
|
onTagSelected: onTagSelected,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ abstract class DocumentItem extends StatelessWidget {
|
|||||||
final bool isLabelClickable;
|
final bool isLabelClickable;
|
||||||
final bool enableHeroAnimation;
|
final bool enableHeroAnimation;
|
||||||
|
|
||||||
|
final Map<int, Tag> tags;
|
||||||
|
final Map<int, Correspondent> correspondents;
|
||||||
|
final Map<int, DocumentType> documentTypes;
|
||||||
|
final Map<int, StoragePath> storagePaths;
|
||||||
|
|
||||||
final void Function(int tagId)? onTagSelected;
|
final void Function(int tagId)? onTagSelected;
|
||||||
final void Function(int? correspondentId)? onCorrespondentSelected;
|
final void Function(int? correspondentId)? onCorrespondentSelected;
|
||||||
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
||||||
@@ -28,5 +33,9 @@ abstract class DocumentItem extends StatelessWidget {
|
|||||||
this.onDocumentTypeSelected,
|
this.onDocumentTypeSelected,
|
||||||
this.onStoragePathSelected,
|
this.onStoragePathSelected,
|
||||||
required this.enableHeroAnimation,
|
required this.enableHeroAnimation,
|
||||||
|
required this.tags,
|
||||||
|
required this.correspondents,
|
||||||
|
required this.documentTypes,
|
||||||
|
required this.storagePaths,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
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_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';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/cubit/providers/document_type_bloc_provider.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
|
|
||||||
@@ -25,11 +21,15 @@ class DocumentListItem extends DocumentItem {
|
|||||||
super.onTagSelected,
|
super.onTagSelected,
|
||||||
super.onTap,
|
super.onTap,
|
||||||
super.enableHeroAnimation = true,
|
super.enableHeroAnimation = true,
|
||||||
|
required super.tags,
|
||||||
|
required super.correspondents,
|
||||||
|
required super.documentTypes,
|
||||||
|
required super.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DocumentTypeBlocProvider(
|
return Material(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
@@ -46,7 +46,7 @@ class DocumentListItem extends DocumentItem {
|
|||||||
absorbing: isSelectionActive,
|
absorbing: isSelectionActive,
|
||||||
child: CorrespondentWidget(
|
child: CorrespondentWidget(
|
||||||
isClickable: isLabelClickable,
|
isClickable: isLabelClickable,
|
||||||
correspondentId: document.correspondent,
|
correspondent: correspondents[document.correspondent],
|
||||||
onSelected: onCorrespondentSelected,
|
onSelected: onCorrespondentSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -61,62 +61,59 @@ class DocumentListItem extends DocumentItem {
|
|||||||
absorbing: isSelectionActive,
|
absorbing: isSelectionActive,
|
||||||
child: TagsWidget(
|
child: TagsWidget(
|
||||||
isClickable: isLabelClickable,
|
isClickable: isLabelClickable,
|
||||||
tagIds: document.tags,
|
tags: document.tags
|
||||||
|
.where((e) => tags.containsKey(e))
|
||||||
|
.map((e) => tags[e]!)
|
||||||
|
.toList(),
|
||||||
isMultiLine: false,
|
isMultiLine: false,
|
||||||
onTagSelected: (id) => onTagSelected?.call(id),
|
onTagSelected: (id) => onTagSelected?.call(id),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child:
|
child: RichText(
|
||||||
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
maxLines: 1,
|
||||||
builder: (context, docTypes) {
|
overflow: TextOverflow.ellipsis,
|
||||||
return RichText(
|
text: TextSpan(
|
||||||
maxLines: 1,
|
text: DateFormat.yMMMd().format(document.created),
|
||||||
overflow: TextOverflow.ellipsis,
|
style: Theme.of(context)
|
||||||
text: TextSpan(
|
.textTheme
|
||||||
text: DateFormat.yMMMd().format(document.created),
|
.labelSmall
|
||||||
style: Theme.of(context)
|
?.apply(color: Colors.grey),
|
||||||
.textTheme
|
children: document.documentType != null
|
||||||
.labelSmall
|
? [
|
||||||
?.apply(color: Colors.grey),
|
const TextSpan(text: '\u30FB'),
|
||||||
children: document.documentType != null
|
TextSpan(
|
||||||
? [
|
text: documentTypes[document.documentType]?.name,
|
||||||
const TextSpan(text: '\u30FB'),
|
),
|
||||||
TextSpan(
|
]
|
||||||
text:
|
: null,
|
||||||
docTypes.labels[document.documentType]?.name,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
// Row(
|
|
||||||
// children: [
|
|
||||||
// Text(
|
|
||||||
// DateFormat.yMMMd().format(document.created),
|
|
||||||
// style: Theme.of(context)
|
|
||||||
// .textTheme
|
|
||||||
// .bodySmall
|
|
||||||
// ?.apply(color: Colors.grey),
|
|
||||||
// ),
|
|
||||||
// if (document.documentType != null) ...[
|
|
||||||
// Text("\u30FB"),
|
|
||||||
// DocumentTypeWidget(
|
|
||||||
// documentTypeId: document.documentType,
|
|
||||||
// textStyle: Theme.of(context).textTheme.bodySmall?.apply(
|
|
||||||
// color: Colors.grey,
|
|
||||||
// overflow: TextOverflow.ellipsis,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
// Row(
|
||||||
|
// children: [
|
||||||
|
// Text(
|
||||||
|
// DateFormat.yMMMd().format(document.created),
|
||||||
|
// style: Theme.of(context)
|
||||||
|
// .textTheme
|
||||||
|
// .bodySmall
|
||||||
|
// ?.apply(color: Colors.grey),
|
||||||
|
// ),
|
||||||
|
// if (document.documentType != null) ...[
|
||||||
|
// Text("\u30FB"),
|
||||||
|
// DocumentTypeWidget(
|
||||||
|
// documentTypeId: document.documentType,
|
||||||
|
// textStyle: Theme.of(context).textTheme.bodySmall?.apply(
|
||||||
|
// color: Colors.grey,
|
||||||
|
// overflow: TextOverflow.ellipsis,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
),
|
||||||
isThreeLine: document.tags.isNotEmpty,
|
isThreeLine: document.tags.isNotEmpty,
|
||||||
leading: AspectRatio(
|
leading: AspectRatio(
|
||||||
aspectRatio: _a4AspectRatio,
|
aspectRatio: _a4AspectRatio,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.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/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';
|
||||||
|
|
||||||
@@ -49,6 +50,11 @@ class DocumentFilterForm extends StatefulWidget {
|
|||||||
final DocumentFilter initialFilter;
|
final DocumentFilter initialFilter;
|
||||||
final ScrollController? scrollController;
|
final ScrollController? scrollController;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
final Map<int, Correspondent> correspondents;
|
||||||
|
final Map<int, DocumentType> documentTypes;
|
||||||
|
final Map<int, Tag> tags;
|
||||||
|
final Map<int, StoragePath> storagePaths;
|
||||||
|
|
||||||
const DocumentFilterForm({
|
const DocumentFilterForm({
|
||||||
super.key,
|
super.key,
|
||||||
this.header,
|
this.header,
|
||||||
@@ -56,6 +62,10 @@ class DocumentFilterForm extends StatefulWidget {
|
|||||||
required this.initialFilter,
|
required this.initialFilter,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
required this.correspondents,
|
||||||
|
required this.documentTypes,
|
||||||
|
required this.tags,
|
||||||
|
required this.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -80,7 +90,7 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
slivers: [
|
slivers: [
|
||||||
if (widget.header != null) widget.header!,
|
if (widget.header != null) widget.header!,
|
||||||
..._buildFormFieldList(),
|
..._buildFormFieldList(),
|
||||||
SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
@@ -145,47 +155,32 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentTypeFormField() {
|
Widget _buildDocumentTypeFormField() {
|
||||||
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
return LabelFormField<DocumentType>(
|
||||||
builder: (context, state) {
|
name: DocumentFilterForm.fkDocumentType,
|
||||||
return LabelFormField<DocumentType>(
|
options: widget.documentTypes,
|
||||||
formBuilderState: widget.formKey.currentState,
|
labelText: S.of(context)!.documentType,
|
||||||
name: DocumentFilterForm.fkDocumentType,
|
initialValue: widget.initialFilter.documentType,
|
||||||
labelOptions: state.labels,
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
textFieldLabel: S.of(context)!.documentType,
|
|
||||||
initialValue: widget.initialFilter.documentType,
|
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCorrespondentFormField() {
|
Widget _buildCorrespondentFormField() {
|
||||||
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
|
return LabelFormField<Correspondent>(
|
||||||
builder: (context, state) {
|
name: DocumentFilterForm.fkCorrespondent,
|
||||||
return LabelFormField<Correspondent>(
|
options: widget.correspondents,
|
||||||
formBuilderState: widget.formKey.currentState,
|
labelText: S.of(context)!.correspondent,
|
||||||
name: DocumentFilterForm.fkCorrespondent,
|
initialValue: widget.initialFilter.correspondent,
|
||||||
labelOptions: state.labels,
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
textFieldLabel: S.of(context)!.correspondent,
|
|
||||||
initialValue: widget.initialFilter.correspondent,
|
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStoragePathFormField() {
|
Widget _buildStoragePathFormField() {
|
||||||
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
|
return LabelFormField<StoragePath>(
|
||||||
builder: (context, state) {
|
name: DocumentFilterForm.fkStoragePath,
|
||||||
return LabelFormField<StoragePath>(
|
options: widget.storagePaths,
|
||||||
formBuilderState: widget.formKey.currentState,
|
labelText: S.of(context)!.storagePath,
|
||||||
name: DocumentFilterForm.fkStoragePath,
|
initialValue: widget.initialFilter.storagePath,
|
||||||
labelOptions: state.labels,
|
prefixIcon: const Icon(Icons.folder_outlined),
|
||||||
textFieldLabel: S.of(context)!.storagePath,
|
|
||||||
initialValue: widget.initialFilter.storagePath,
|
|
||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,16 +192,14 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
|
Widget _buildTagsFormField() {
|
||||||
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
|
return TagsFormField(
|
||||||
builder: (context, state) {
|
name: DocumentModel.tagsKey,
|
||||||
return TagFormField(
|
initialValue: widget.initialFilter.tags,
|
||||||
name: DocumentModel.tagsKey,
|
options: widget.tags,
|
||||||
initialValue: widget.initialFilter.tags,
|
allowExclude: false,
|
||||||
allowCreation: false,
|
allowOnlySelection: false,
|
||||||
selectableOptions: state.labels,
|
allowCreation: false,
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,20 @@ class DocumentFilterPanel extends StatefulWidget {
|
|||||||
final DocumentFilter initialFilter;
|
final DocumentFilter initialFilter;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
final DraggableScrollableController draggableSheetController;
|
final DraggableScrollableController draggableSheetController;
|
||||||
|
final Map<int, Correspondent> correspondents;
|
||||||
|
final Map<int, DocumentType> documentTypes;
|
||||||
|
final Map<int, Tag> tags;
|
||||||
|
final Map<int, StoragePath> storagePaths;
|
||||||
|
|
||||||
const DocumentFilterPanel({
|
const DocumentFilterPanel({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.initialFilter,
|
required this.initialFilter,
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
required this.draggableSheetController,
|
required this.draggableSheetController,
|
||||||
|
required this.correspondents,
|
||||||
|
required this.documentTypes,
|
||||||
|
required this.tags,
|
||||||
|
required this.storagePaths,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -38,10 +47,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
|
|
||||||
void animateTitleByDrag() {
|
void animateTitleByDrag() {
|
||||||
setState(
|
setState(
|
||||||
() {
|
() => _heightAnimationValue =
|
||||||
_heightAnimationValue = dp(
|
dp(((max(0.9, widget.draggableSheetController.size) - 0.9) / 0.1), 5),
|
||||||
((max(0.9, widget.draggableSheetController.size) - 0.9) / 0.1), 5);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +103,10 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
scrollController: widget.scrollController,
|
scrollController: widget.scrollController,
|
||||||
initialFilter: widget.initialFilter,
|
initialFilter: widget.initialFilter,
|
||||||
header: _buildPanelHeader(),
|
header: _buildPanelHeader(),
|
||||||
|
correspondents: widget.correspondents,
|
||||||
|
documentTypes: widget.documentTypes,
|
||||||
|
storagePaths: widget.storagePaths,
|
||||||
|
tags: widget.tags,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|||||||
class SortFieldSelectionBottomSheet extends StatefulWidget {
|
class SortFieldSelectionBottomSheet extends StatefulWidget {
|
||||||
final SortOrder initialSortOrder;
|
final SortOrder initialSortOrder;
|
||||||
final SortField? initialSortField;
|
final SortField? initialSortField;
|
||||||
|
final Map<int, Correspondent> correspondents;
|
||||||
|
final Map<int, DocumentType> documentTypes;
|
||||||
|
final Map<int, Tag> tags;
|
||||||
|
final Map<int, StoragePath> storagePaths;
|
||||||
|
|
||||||
final Future Function(SortField? field, SortOrder order) onSubmit;
|
final Future Function(SortField? field, SortOrder order) onSubmit;
|
||||||
|
|
||||||
@@ -18,6 +22,10 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
|
|||||||
required this.initialSortOrder,
|
required this.initialSortOrder,
|
||||||
required this.initialSortField,
|
required this.initialSortField,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
|
required this.correspondents,
|
||||||
|
required this.documentTypes,
|
||||||
|
required this.tags,
|
||||||
|
required this.storagePaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -67,31 +75,20 @@ class _SortFieldSelectionBottomSheetState
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_buildSortOption(SortField.archiveSerialNumber),
|
_buildSortOption(SortField.archiveSerialNumber),
|
||||||
BlocBuilder<LabelCubit<Correspondent>,
|
_buildSortOption(
|
||||||
LabelState<Correspondent>>(
|
SortField.correspondentName,
|
||||||
builder: (context, state) {
|
enabled: widget.correspondents.values.fold<bool>(
|
||||||
return _buildSortOption(
|
false,
|
||||||
SortField.correspondentName,
|
(previousValue, element) =>
|
||||||
enabled: state.labels.values.fold<bool>(
|
previousValue || (element.documentCount ?? 0) > 0),
|
||||||
false,
|
|
||||||
(previousValue, element) =>
|
|
||||||
previousValue ||
|
|
||||||
(element.documentCount ?? 0) > 0),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
_buildSortOption(SortField.title),
|
_buildSortOption(SortField.title),
|
||||||
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
_buildSortOption(
|
||||||
builder: (context, state) {
|
SortField.documentType,
|
||||||
return _buildSortOption(
|
enabled: widget.documentTypes.values.fold<bool>(
|
||||||
SortField.documentType,
|
false,
|
||||||
enabled: state.labels.values.fold<bool>(
|
(previousValue, element) =>
|
||||||
false,
|
previousValue || (element.documentCount ?? 0) > 0),
|
||||||
(previousValue, element) =>
|
|
||||||
previousValue ||
|
|
||||||
(element.documentCount ?? 0) > 0),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
_buildSortOption(SortField.created),
|
_buildSortOption(SortField.created),
|
||||||
_buildSortOption(SortField.added),
|
_buildSortOption(SortField.added),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.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/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -29,19 +31,10 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.pop(context, false),
|
DialogConfirmButton(
|
||||||
child: Text(S.of(context)!.cancel),
|
label: S.of(context)!.delete,
|
||||||
),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
TextButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
foregroundColor:
|
|
||||||
MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
child: Text(S.of(context)!.delete),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/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';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
||||||
@@ -19,16 +21,10 @@ class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),
|
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
child: Text(S.of(context)!.cancel),
|
DialogConfirmButton(
|
||||||
onPressed: () => Navigator.pop(context, false),
|
label: S.of(context)!.delete,
|
||||||
),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
TextButton(
|
|
||||||
child: Text(
|
|
||||||
S.of(context)!.delete,
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter/src/widgets/placeholder.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/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/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:provider/provider.dart';
|
|
||||||
|
|
||||||
class DocumentSelectionSliverAppBar extends StatelessWidget {
|
class DocumentSelectionSliverAppBar extends StatelessWidget {
|
||||||
final DocumentsState state;
|
final DocumentsState state;
|
||||||
@@ -15,7 +17,11 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
|
stretch: false,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
title: Text(
|
title: Text(
|
||||||
S.of(context)!.countSelected(state.selection.length),
|
S.of(context)!.countSelected(state.selection.length),
|
||||||
),
|
),
|
||||||
@@ -50,6 +56,181 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(kTextTabBarHeight),
|
||||||
|
child: SizedBox(
|
||||||
|
height: kTextTabBarHeight,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: [
|
||||||
|
ActionChip(
|
||||||
|
label: Text(S.of(context)!.correspondent),
|
||||||
|
avatar: const Icon(Icons.edit),
|
||||||
|
onPressed: () async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider(
|
||||||
|
create: (context) => DocumentBulkActionCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
selection: state.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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).paddedOnly(left: 8, right: 4),
|
||||||
|
ActionChip(
|
||||||
|
label: Text(S.of(context)!.documentType),
|
||||||
|
avatar: const Icon(Icons.edit),
|
||||||
|
onPressed: () async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider(
|
||||||
|
create: (context) => DocumentBulkActionCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
selection: state.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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).paddedOnly(left: 8, right: 4),
|
||||||
|
ActionChip(
|
||||||
|
label: Text(S.of(context)!.storagePath),
|
||||||
|
avatar: const Icon(Icons.edit),
|
||||||
|
onPressed: () async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider(
|
||||||
|
create: (context) => DocumentBulkActionCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
selection: state.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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).paddedOnly(left: 8, right: 4),
|
||||||
|
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBulkEditTagsChip(BuildContext context) {
|
||||||
|
return ActionChip(
|
||||||
|
label: Text(S.of(context)!.tags),
|
||||||
|
avatar: const Icon(Icons.edit),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider(
|
||||||
|
create: (context) => DocumentBulkActionCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
selection: state.selection,
|
||||||
|
),
|
||||||
|
child: Builder(builder: (context) {
|
||||||
|
return const FullscreenBulkEditTagsWidget();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PopupMenuButton<ViewType>(
|
return PopupMenuButton<ViewType>(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 4 * 56.0,
|
||||||
|
maxWidth: 5 * 56.0,
|
||||||
|
), // Ensures text is not split into two lines
|
||||||
position: PopupMenuPosition.under,
|
position: PopupMenuPosition.under,
|
||||||
initialValue: viewType,
|
initialValue: viewType,
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
@@ -70,7 +74,10 @@ class ViewTypeSelectionWidget extends StatelessWidget {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
selected: selected,
|
selected: selected,
|
||||||
trailing: selected ? const Icon(Icons.done) : null,
|
trailing: selected ? const Icon(Icons.done) : null,
|
||||||
title: Text(label),
|
title: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||||
textColor: Theme.of(context).colorScheme.onSurface,
|
textColor: Theme.of(context).colorScheme.onSurface,
|
||||||
leading: Icon(icon),
|
leading: Icon(icon),
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import 'package:paperless_mobile/features/documents/view/widgets/search/sort_fie
|
|||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
|
|
||||||
class SortDocumentsButton extends StatelessWidget {
|
class SortDocumentsButton extends StatelessWidget {
|
||||||
|
final bool enabled;
|
||||||
const SortDocumentsButton({
|
const SortDocumentsButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.enabled = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -24,47 +26,47 @@ class SortDocumentsButton extends StatelessWidget {
|
|||||||
? Icons.arrow_upward
|
? Icons.arrow_upward
|
||||||
: Icons.arrow_downward),
|
: Icons.arrow_downward),
|
||||||
label: Text(translateSortField(context, state.filter.sortField)),
|
label: Text(translateSortField(context, state.filter.sortField)),
|
||||||
onPressed: () {
|
onPressed: enabled
|
||||||
showModalBottomSheet(
|
? () {
|
||||||
elevation: 2,
|
showModalBottomSheet(
|
||||||
context: context,
|
elevation: 2,
|
||||||
isScrollControlled: true,
|
context: context,
|
||||||
shape: const RoundedRectangleBorder(
|
isScrollControlled: true,
|
||||||
borderRadius: BorderRadius.only(
|
shape: const RoundedRectangleBorder(
|
||||||
topLeft: Radius.circular(16),
|
borderRadius: BorderRadius.only(
|
||||||
topRight: Radius.circular(16),
|
topLeft: Radius.circular(16),
|
||||||
),
|
topRight: Radius.circular(16),
|
||||||
),
|
|
||||||
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
|
||||||
value: context.read<DocumentsCubit>(),
|
|
||||||
child: MultiBlocProvider(
|
|
||||||
providers: [
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) => LabelCubit<DocumentType>(
|
|
||||||
context.read<LabelRepository<DocumentType>>(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
||||||
create: (context) => LabelCubit<Correspondent>(
|
value: context.read<DocumentsCubit>(),
|
||||||
context.read<LabelRepository<Correspondent>>(),
|
child: MultiBlocProvider(
|
||||||
),
|
providers: [
|
||||||
),
|
BlocProvider(
|
||||||
],
|
create: (context) => LabelCubit(context.read()),
|
||||||
child: SortFieldSelectionBottomSheet(
|
),
|
||||||
initialSortField: state.filter.sortField,
|
],
|
||||||
initialSortOrder: state.filter.sortOrder,
|
child: SortFieldSelectionBottomSheet(
|
||||||
onSubmit: (field, order) =>
|
initialSortField: state.filter.sortField,
|
||||||
context.read<DocumentsCubit>().updateCurrentFilter(
|
initialSortOrder: state.filter.sortOrder,
|
||||||
(filter) => filter.copyWith(
|
onSubmit: (field, order) => context
|
||||||
sortField: field,
|
.read<DocumentsCubit>()
|
||||||
sortOrder: order,
|
.updateCurrentFilter(
|
||||||
|
(filter) => filter.copyWith(
|
||||||
|
sortField: field,
|
||||||
|
sortOrder: order,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
correspondents: state.correspondents,
|
||||||
),
|
documentTypes: state.documentTypes,
|
||||||
),
|
storagePaths: state.storagePaths,
|
||||||
),
|
tags: state.tags,
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/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:freezed_annotation/freezed_annotation.dart';
|
||||||
part 'edit_label_state.dart';
|
part 'edit_label_state.dart';
|
||||||
|
part 'edit_label_cubit.freezed.dart';
|
||||||
|
|
||||||
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
|
class EditLabelCubit extends Cubit<EditLabelState>
|
||||||
final LabelRepository<T> _repository;
|
with LabelCubitMixin<EditLabelState> {
|
||||||
|
@override
|
||||||
|
final LabelRepository labelRepository;
|
||||||
|
|
||||||
StreamSubscription? _subscription;
|
EditLabelCubit(this.labelRepository) : super(const EditLabelState()) {
|
||||||
|
labelRepository.addListener(
|
||||||
EditLabelCubit(LabelRepository<T> repository)
|
this,
|
||||||
: _repository = repository,
|
onChanged: (labels) => state.copyWith(
|
||||||
super(EditLabelState<T>(labels: repository.current?.values ?? {})) {
|
correspondents: labels.correspondents,
|
||||||
_subscription = repository.values.listen(
|
documentTypes: labels.documentTypes,
|
||||||
(event) => emit(EditLabelState(labels: event?.values ?? {})),
|
tags: labels.tags,
|
||||||
|
storagePaths: labels.storagePaths,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> create(T label) => _repository.create(label);
|
|
||||||
|
|
||||||
Future<T> update(T label) => _repository.update(label);
|
|
||||||
|
|
||||||
Future<void> delete(T label) => _repository.delete(label);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_subscription?.cancel();
|
labelRepository.removeListener(this);
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user