mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 07:15:47 -06:00
Updated onboarding, reformatted files, improved referenced documents view, updated error handling
This commit is contained in:
BIN
assets/images/documents_headache.png
Normal file
BIN
assets/images/documents_headache.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/images/organize_documents.png
Normal file
BIN
assets/images/organize_documents.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/images/secure_documents.png
Normal file
BIN
assets/images/secure_documents.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/success.png
Normal file
BIN
assets/images/success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
@@ -9,13 +9,20 @@ class ConnectivityCubit extends Cubit<ConnectivityState> {
|
|||||||
final ConnectivityStatusService connectivityStatusService;
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
late final StreamSubscription<bool> _sub;
|
late final StreamSubscription<bool> _sub;
|
||||||
|
|
||||||
ConnectivityCubit(this.connectivityStatusService) : super(ConnectivityState.undefined);
|
ConnectivityCubit(this.connectivityStatusService)
|
||||||
|
: super(ConnectivityState.undefined);
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
final bool isConnected = await connectivityStatusService.isConnectedToInternet();
|
final bool isConnected =
|
||||||
emit(isConnected ? ConnectivityState.connected : ConnectivityState.notConnected);
|
await connectivityStatusService.isConnectedToInternet();
|
||||||
_sub = connectivityStatusService.connectivityChanges().listen((isConnected) {
|
emit(isConnected
|
||||||
emit(isConnected ? ConnectivityState.connected : ConnectivityState.notConnected);
|
? ConnectivityState.connected
|
||||||
|
: ConnectivityState.notConnected);
|
||||||
|
_sub =
|
||||||
|
connectivityStatusService.connectivityChanges().listen((isConnected) {
|
||||||
|
emit(isConnected
|
||||||
|
? ConnectivityState.connected
|
||||||
|
: ConnectivityState.notConnected);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
lib/core/bloc/global_error_cubit.dart
Normal file
46
lib/core/bloc/global_error_cubit.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Class for handling generic errors which usually only require to inform the user via a Snackbar
|
||||||
|
/// or similar that an error has occurred.
|
||||||
|
///
|
||||||
|
@singleton
|
||||||
|
class GlobalErrorCubit extends Cubit<GlobalErrorState> {
|
||||||
|
static const _waitBeforeNextErrorDuration = Duration(seconds: 5);
|
||||||
|
|
||||||
|
GlobalErrorCubit() : super(GlobalErrorState.initial);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Adds a new error to this bloc. If the new error is equal to the current error, the new error
|
||||||
|
/// will not be published unless the previous error occured over 5 seconds ago.
|
||||||
|
///
|
||||||
|
void add(ErrorMessage error) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (error != state.error || (error == state.error && _canEmitNewError())) {
|
||||||
|
emit(GlobalErrorState(error: error, errorTimestamp: now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _canEmitNewError() {
|
||||||
|
if (state.errorTimestamp != null) {
|
||||||
|
return DateTime.now().difference(state.errorTimestamp!).inSeconds >= 5;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
emit(GlobalErrorState.initial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlobalErrorState {
|
||||||
|
static const GlobalErrorState initial = GlobalErrorState();
|
||||||
|
final ErrorMessage? error;
|
||||||
|
final DateTime? errorTimestamp;
|
||||||
|
|
||||||
|
const GlobalErrorState({this.error, this.errorTimestamp});
|
||||||
|
|
||||||
|
bool get hasError => error != null;
|
||||||
|
}
|
||||||
@@ -1,40 +1,75 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
import 'package:paperless_mobile/features/labels/repository/label_repository.dart';
|
import 'package:paperless_mobile/features/labels/repository/label_repository.dart';
|
||||||
|
|
||||||
abstract class LabelCubit<T extends Label> extends Cubit<Map<int, T>> {
|
abstract class LabelCubit<T extends Label> extends Cubit<Map<int, T>> {
|
||||||
final LabelRepository labelRepository;
|
final LabelRepository labelRepository;
|
||||||
LabelCubit(this.labelRepository) : super({});
|
final GlobalErrorCubit errorCubit;
|
||||||
|
|
||||||
|
LabelCubit(this.labelRepository, this.errorCubit) : super({});
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void loadFrom(Iterable<T> items) => emit(Map.fromIterable(items, key: (e) => (e as T).id!));
|
void loadFrom(Iterable<T> items) =>
|
||||||
|
emit(Map.fromIterable(items, key: (e) => (e as T).id!));
|
||||||
|
|
||||||
Future<T> add(T item) async {
|
Future<T> add(
|
||||||
|
T item, {
|
||||||
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
assert(item.id == null);
|
assert(item.id == null);
|
||||||
final addedItem = await save(item);
|
try {
|
||||||
final newState = {...state};
|
final addedItem = await save(item);
|
||||||
newState.putIfAbsent(addedItem.id!, () => addedItem);
|
final newState = {...state};
|
||||||
emit(newState);
|
newState.putIfAbsent(addedItem.id!, () => addedItem);
|
||||||
return addedItem;
|
emit(newState);
|
||||||
|
return addedItem;
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
return Future.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> replace(T item) async {
|
Future<T> replace(
|
||||||
|
T item, {
|
||||||
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
assert(item.id != null);
|
assert(item.id != null);
|
||||||
final updatedItem = await update(item);
|
try {
|
||||||
final newState = {...state};
|
final updatedItem = await update(item);
|
||||||
newState[item.id!] = updatedItem;
|
final newState = {...state};
|
||||||
emit(newState);
|
newState[item.id!] = updatedItem;
|
||||||
return updatedItem;
|
emit(newState);
|
||||||
|
return updatedItem;
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
return Future.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> remove(T item) async {
|
Future<void> remove(
|
||||||
|
T item, {
|
||||||
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
assert(item.id != null);
|
assert(item.id != null);
|
||||||
if (state.containsKey(item.id)) {
|
if (state.containsKey(item.id)) {
|
||||||
final deletedId = await delete(item);
|
try {
|
||||||
final newState = {...state};
|
final deletedId = await delete(item);
|
||||||
newState.remove(deletedId);
|
final newState = {...state};
|
||||||
emit(newState);
|
newState.remove(deletedId);
|
||||||
|
emit(newState);
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
return Future.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,19 @@ class AuthenticationInterceptor implements InterceptorContract {
|
|||||||
}
|
}
|
||||||
return request.copyWith(
|
return request.copyWith(
|
||||||
//Append server Url
|
//Append server Url
|
||||||
url: Uri.parse(authState.authentication!.serverUrl + request.url.toString()),
|
url: Uri.parse(
|
||||||
|
authState.authentication!.serverUrl + request.url.toString()),
|
||||||
headers: authState.authentication!.token.isEmpty
|
headers: authState.authentication!.token.isEmpty
|
||||||
? request.headers
|
? request.headers
|
||||||
: {...request.headers, 'Authorization': 'Token ${authState.authentication!.token}'},
|
: {
|
||||||
|
...request.headers,
|
||||||
|
'Authorization': 'Token ${authState.authentication!.token}'
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async => response;
|
Future<BaseResponse> interceptResponse(
|
||||||
|
{required BaseResponse response}) async =>
|
||||||
|
response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,22 @@ import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
|||||||
import 'package:http_interceptor/http_interceptor.dart';
|
import 'package:http_interceptor/http_interceptor.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
@Deprecated("Delegated to TimeoutClient!")
|
||||||
@injectable
|
@injectable
|
||||||
class ConnectionStateInterceptor implements InterceptorContract {
|
class ConnectionStateInterceptor implements InterceptorContract {
|
||||||
final AuthenticationCubit authenticationCubit;
|
final AuthenticationCubit authenticationCubit;
|
||||||
final ConnectivityStatusService connectivityStatusService;
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
ConnectionStateInterceptor(this.authenticationCubit, this.connectivityStatusService);
|
|
||||||
|
ConnectionStateInterceptor(
|
||||||
|
this.authenticationCubit, this.connectivityStatusService);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
||||||
if (!(await connectivityStatusService.isConnectedToInternet())) {
|
|
||||||
throw const ErrorMessage(ErrorCode.deviceOffline);
|
|
||||||
}
|
|
||||||
final isServerReachable = await connectivityStatusService.isServerReachable(request.url.origin);
|
|
||||||
if (!isServerReachable) {
|
|
||||||
throw const ErrorMessage(ErrorCode.serverUnreachable);
|
|
||||||
}
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async => response;
|
Future<BaseResponse> interceptResponse(
|
||||||
|
{required BaseResponse response}) async =>
|
||||||
|
response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ class LanguageHeaderInterceptor implements InterceptorContract {
|
|||||||
if (appSettingsCubit.state.preferredLocaleSubtag == "en") {
|
if (appSettingsCubit.state.preferredLocaleSubtag == "en") {
|
||||||
languages = "en";
|
languages = "en";
|
||||||
} else {
|
} else {
|
||||||
languages = appSettingsCubit.state.preferredLocaleSubtag + ",en;q=0.7,en-US;q=0.6";
|
languages = appSettingsCubit.state.preferredLocaleSubtag +
|
||||||
|
",en;q=0.7,en-US;q=0.6";
|
||||||
}
|
}
|
||||||
request.headers.addAll({"Accept-Language": languages});
|
request.headers.addAll({"Accept-Language": languages});
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async => response;
|
Future<BaseResponse> interceptResponse(
|
||||||
|
{required BaseResponse response}) async =>
|
||||||
|
response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ const interceptedRoutes = ['thumb/'];
|
|||||||
@injectable
|
@injectable
|
||||||
class ResponseConversionInterceptor implements InterceptorContract {
|
class ResponseConversionInterceptor implements InterceptorContract {
|
||||||
@override
|
@override
|
||||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async => request;
|
Future<BaseRequest> interceptRequest({required BaseRequest request}) async =>
|
||||||
|
request;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<BaseResponse> interceptResponse({required BaseResponse response}) async {
|
Future<BaseResponse> interceptResponse(
|
||||||
final String requestUrl = response.request?.url.toString().split("?").first ?? '';
|
{required BaseResponse response}) async {
|
||||||
|
final String requestUrl =
|
||||||
|
response.request?.url.toString().split("?").first ?? '';
|
||||||
if (response.request?.method == "GET" &&
|
if (response.request?.method == "GET" &&
|
||||||
interceptedRoutes.any((element) => requestUrl.endsWith(element))) {
|
interceptedRoutes.any((element) => requestUrl.endsWith(element))) {
|
||||||
final resp = response as Response;
|
final resp = response as Response;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import 'dart:typed_data';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
|
||||||
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
@@ -14,13 +15,17 @@ import 'package:injectable/injectable.dart';
|
|||||||
@Injectable(as: BaseClient)
|
@Injectable(as: BaseClient)
|
||||||
@Named("timeoutClient")
|
@Named("timeoutClient")
|
||||||
class TimeoutClient implements BaseClient {
|
class TimeoutClient implements BaseClient {
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
static const Duration requestTimeout = Duration(seconds: 25);
|
static const Duration requestTimeout = Duration(seconds: 25);
|
||||||
|
|
||||||
|
TimeoutClient(this.connectivityStatusService);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<StreamedResponse> send(BaseRequest request) async {
|
Future<StreamedResponse> send(BaseRequest request) async {
|
||||||
return getIt<BaseClient>().send(request).timeout(
|
return getIt<BaseClient>().send(request).timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,32 +37,38 @@ class TimeoutClient implements BaseClient {
|
|||||||
@override
|
@override
|
||||||
Future<Response> delete(Uri url,
|
Future<Response> delete(Uri url,
|
||||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return _handle400Error(
|
return _handle400Error(
|
||||||
await getIt<BaseClient>()
|
await getIt<BaseClient>()
|
||||||
.delete(url, headers: headers, body: body, encoding: encoding)
|
.delete(url, headers: headers, body: body, encoding: encoding)
|
||||||
.timeout(
|
.timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Response> get(Uri url, {Map<String, String>? headers}) async {
|
Future<Response> get(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return _handle400Error(
|
return _handle400Error(
|
||||||
await getIt<BaseClient>().get(url, headers: headers).timeout(
|
await getIt<BaseClient>().get(url, headers: headers).timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Response> head(Uri url, {Map<String, String>? headers}) async {
|
Future<Response> head(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return _handle400Error(
|
return _handle400Error(
|
||||||
await getIt<BaseClient>().head(url, headers: headers).timeout(
|
await getIt<BaseClient>().head(url, headers: headers).timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -65,12 +76,14 @@ class TimeoutClient implements BaseClient {
|
|||||||
@override
|
@override
|
||||||
Future<Response> patch(Uri url,
|
Future<Response> patch(Uri url,
|
||||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return _handle400Error(
|
return _handle400Error(
|
||||||
await getIt<BaseClient>()
|
await getIt<BaseClient>()
|
||||||
.patch(url, headers: headers, body: body, encoding: encoding)
|
.patch(url, headers: headers, body: body, encoding: encoding)
|
||||||
.timeout(
|
.timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,10 +91,14 @@ class TimeoutClient implements BaseClient {
|
|||||||
@override
|
@override
|
||||||
Future<Response> post(Uri url,
|
Future<Response> post(Uri url,
|
||||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return _handle400Error(
|
return _handle400Error(
|
||||||
await getIt<BaseClient>().post(url, headers: headers, body: body, encoding: encoding).timeout(
|
await getIt<BaseClient>()
|
||||||
|
.post(url, headers: headers, body: body, encoding: encoding)
|
||||||
|
.timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,27 +106,35 @@ class TimeoutClient implements BaseClient {
|
|||||||
@override
|
@override
|
||||||
Future<Response> put(Uri url,
|
Future<Response> put(Uri url,
|
||||||
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return _handle400Error(
|
return _handle400Error(
|
||||||
await getIt<BaseClient>().put(url, headers: headers, body: body, encoding: encoding).timeout(
|
await getIt<BaseClient>()
|
||||||
|
.put(url, headers: headers, body: body, encoding: encoding)
|
||||||
|
.timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> read(Uri url, {Map<String, String>? headers}) async {
|
Future<String> read(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return getIt<BaseClient>().read(url, headers: headers).timeout(
|
return getIt<BaseClient>().read(url, headers: headers).timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) {
|
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
await _handleOfflineState();
|
||||||
return getIt<BaseClient>().readBytes(url, headers: headers).timeout(
|
return getIt<BaseClient>().readBytes(url, headers: headers).timeout(
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
onTimeout: () => Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
onTimeout: () =>
|
||||||
|
Future.error(const ErrorMessage(ErrorCode.requestTimedOut)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,11 +142,12 @@ class TimeoutClient implements BaseClient {
|
|||||||
if (response.statusCode == 400) {
|
if (response.statusCode == 400) {
|
||||||
// try to parse contained error message, otherwise return response
|
// try to parse contained error message, otherwise return response
|
||||||
final JSON json = jsonDecode(utf8.decode(response.bodyBytes));
|
final JSON json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
final Map<String, String> errorMessages = {};
|
final PaperlessValidationErrors errorMessages = {};
|
||||||
//TODO: This could be simplified, look at error message format of paperless-ngx
|
//TODO: This could be simplified, look at error message format of paperless-ngx
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
if (entry.value is List) {
|
if (entry.value is List) {
|
||||||
errorMessages.putIfAbsent(entry.key, () => (entry.value as List).cast<String>().first);
|
errorMessages.putIfAbsent(
|
||||||
|
entry.key, () => (entry.value as List).cast<String>().first);
|
||||||
} else if (entry.value is String) {
|
} else if (entry.value is String) {
|
||||||
errorMessages.putIfAbsent(entry.key, () => entry.value);
|
errorMessages.putIfAbsent(entry.key, () => entry.value);
|
||||||
} else {
|
} else {
|
||||||
@@ -132,4 +158,10 @@ class TimeoutClient implements BaseClient {
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleOfflineState() async {
|
||||||
|
if (!(await connectivityStatusService.isConnectedToInternet())) {
|
||||||
|
throw const ErrorMessage(ErrorCode.deviceOffline);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<bool> connectivityChanges() {
|
Stream<bool> connectivityChanges() {
|
||||||
return connectivity.onConnectivityChanged.map(_hasActiveInternetConnection).asBroadcastStream();
|
return connectivity.onConnectivityChanged
|
||||||
|
.map(_hasActiveInternetConnection)
|
||||||
|
.asBroadcastStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isConnectedToInternet() async {
|
Future<bool> isConnectedToInternet() async {
|
||||||
return _hasActiveInternetConnection(await (Connectivity().checkConnectivity()));
|
return _hasActiveInternetConnection(
|
||||||
|
await (Connectivity().checkConnectivity()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import 'package:injectable/injectable.dart';
|
|||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
abstract class StatusService {
|
abstract class StatusService {
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
Future<void> startListeningBeforeDocumentUpload(String httpUrl,
|
||||||
String httpUrl, AuthenticationInformation credentials, String documentFileName);
|
AuthenticationInformation credentials, String documentFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton(as: StatusService)
|
@Singleton(as: StatusService)
|
||||||
@@ -86,9 +86,11 @@ class LongPollingStatusService implements StatusService {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
final response = await httpClient.get(
|
final response = await httpClient.get(
|
||||||
Uri.parse('$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
|
Uri.parse(
|
||||||
|
'$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
|
||||||
);
|
);
|
||||||
final data = PagedSearchResult.fromJson(jsonDecode(response.body), DocumentModel.fromJson);
|
final data = PagedSearchResult.fromJson(
|
||||||
|
jsonDecode(response.body), DocumentModel.fromJson);
|
||||||
if (data.count > 0) {
|
if (data.count > 0) {
|
||||||
consumptionFinished = true;
|
consumptionFinished = true;
|
||||||
final docId = data.results[0].id;
|
final docId = data.results[0].id;
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ class LocalVault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ClientCertificate?> loadCertificate() async {
|
Future<ClientCertificate?> loadCertificate() async {
|
||||||
return loadAuthenticationInformation().then((value) => value?.clientCertificate);
|
return loadAuthenticationInformation()
|
||||||
|
.then((value) => value?.clientCertificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
|
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
|
||||||
return sharedPreferences.setString(applicationSettingsKey, json.encode(settings.toJson()));
|
return sharedPreferences.setString(
|
||||||
|
applicationSettingsKey, json.encode(settings.toJson()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApplicationSettingsState?> loadApplicationSettings() async {
|
Future<ApplicationSettingsState?> loadApplicationSettings() async {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
typedef JSON = Map<String, dynamic>;
|
|
||||||
2
lib/core/type/types.dart
Normal file
2
lib/core/type/types.dart
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
typedef JSON = Map<String, dynamic>;
|
||||||
|
typedef PaperlessValidationErrors = Map<String, String>;
|
||||||
@@ -4,7 +4,7 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:paperless_mobile/core/logic/timeout_client.dart';
|
import 'package:paperless_mobile/core/logic/timeout_client.dart';
|
||||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -45,7 +45,10 @@ Future<List<T>> getCollection<T>(
|
|||||||
if (body['count'] == 0) {
|
if (body['count'] == 0) {
|
||||||
return <T>[];
|
return <T>[];
|
||||||
} else {
|
} else {
|
||||||
return body['results'].cast<JSON>().map<T>((result) => fromJson(result)).toList();
|
return body['results']
|
||||||
|
.cast<JSON>()
|
||||||
|
.map<T>((result) => fromJson(result))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class ElevatedConfirmationButton extends StatefulWidget {
|
class ElevatedConfirmationButton extends StatefulWidget {
|
||||||
factory ElevatedConfirmationButton.icon(BuildContext context,
|
factory ElevatedConfirmationButton.icon(BuildContext context,
|
||||||
{required void Function() onPressed, required Icon icon, required Widget label}) {
|
{required void Function() onPressed,
|
||||||
|
required Icon icon,
|
||||||
|
required Widget label}) {
|
||||||
final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
|
final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
|
||||||
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
|
final double gap =
|
||||||
|
scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
|
||||||
return ElevatedConfirmationButton(
|
return ElevatedConfirmationButton(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -30,10 +33,12 @@ class ElevatedConfirmationButton extends StatefulWidget {
|
|||||||
final Widget child;
|
final Widget child;
|
||||||
final Widget confirmWidget;
|
final Widget confirmWidget;
|
||||||
@override
|
@override
|
||||||
State<ElevatedConfirmationButton> createState() => _ElevatedConfirmationButtonState();
|
State<ElevatedConfirmationButton> createState() =>
|
||||||
|
_ElevatedConfirmationButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ElevatedConfirmationButtonState extends State<ElevatedConfirmationButton> {
|
class _ElevatedConfirmationButtonState
|
||||||
|
extends State<ElevatedConfirmationButton> {
|
||||||
bool _clickedOnce = false;
|
bool _clickedOnce = false;
|
||||||
double? _originalWidth;
|
double? _originalWidth;
|
||||||
final GlobalKey _originalWidgetKey = GlobalKey();
|
final GlobalKey _originalWidgetKey = GlobalKey();
|
||||||
@@ -46,8 +51,10 @@ class _ElevatedConfirmationButtonState extends State<ElevatedConfirmationButton>
|
|||||||
backgroundColor: MaterialStateProperty.all(widget.color),
|
backgroundColor: MaterialStateProperty.all(widget.color),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_originalWidth =
|
_originalWidth = (_originalWidgetKey.currentContext
|
||||||
(_originalWidgetKey.currentContext?.findRenderObject() as RenderBox).size.width;
|
?.findRenderObject() as RenderBox)
|
||||||
|
.size
|
||||||
|
.width;
|
||||||
setState(() => _clickedOnce = true);
|
setState(() => _clickedOnce = true);
|
||||||
},
|
},
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
|||||||
@@ -38,10 +38,13 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
|||||||
titleLengths[r.nextInt(titleLengths.length - 1)];
|
titleLengths[r.nextInt(titleLengths.length - 1)];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: Container(
|
leading: ClipRRect(
|
||||||
color: Colors.white,
|
borderRadius: BorderRadius.circular(8),
|
||||||
height: 50,
|
child: Container(
|
||||||
width: 50,
|
color: Colors.white,
|
||||||
|
height: 50,
|
||||||
|
width: 35,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
title: Container(
|
title: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
@@ -65,7 +68,7 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
|||||||
spacing: 2.0,
|
spacing: 2.0,
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
tagCount,
|
tagCount,
|
||||||
(index) => Chip(
|
(index) => InputChip(
|
||||||
label: Text(tags[r.nextInt(tags.length)]),
|
label: Text(tags[r.nextInt(tags.length)]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ class HighlightedText extends StatelessWidget {
|
|||||||
int _start = 0;
|
int _start = 0;
|
||||||
|
|
||||||
String _text = caseSensitive ? text : text.toLowerCase();
|
String _text = caseSensitive ? text : text.toLowerCase();
|
||||||
List<String> _highlights =
|
List<String> _highlights = caseSensitive
|
||||||
caseSensitive ? highlights : highlights.map((e) => e.toLowerCase()).toList();
|
? highlights
|
||||||
|
: highlights.map((e) => e.toLowerCase()).toList();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Map<int, String> _highlightsMap = {}; //key (index), value (highlight).
|
Map<int, String> _highlightsMap = {}; //key (index), value (highlight).
|
||||||
@@ -95,7 +96,8 @@ class HighlightedText extends StatelessWidget {
|
|||||||
_spans.add(_highlightSpan(_currentHighlight));
|
_spans.add(_highlightSpan(_currentHighlight));
|
||||||
_start += _currentHighlight.length;
|
_start += _currentHighlight.length;
|
||||||
} else {
|
} else {
|
||||||
_spans.add(_normalSpan(text.substring(_start, _currentIndex), context));
|
_spans
|
||||||
|
.add(_normalSpan(text.substring(_start, _currentIndex), context));
|
||||||
_spans.add(_highlightSpan(_currentHighlight));
|
_spans.add(_highlightSpan(_currentHighlight));
|
||||||
_start = _currentIndex + _currentHighlight.length;
|
_start = _currentIndex + _currentHighlight.length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ class OfflineWidget extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.mood_bad, size: (Theme.of(context).iconTheme.size ?? 24) * 3),
|
Icon(Icons.wifi_off,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
size: (Theme.of(context).iconTheme.size ?? 24) * 3),
|
||||||
Text(
|
Text(
|
||||||
S.of(context).offlineWidgetText,
|
S.of(context).offlineWidgetText,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ abstract class RegisterModule {
|
|||||||
@singleton
|
@singleton
|
||||||
LocalAuthentication get localAuthentication => LocalAuthentication();
|
LocalAuthentication get localAuthentication => LocalAuthentication();
|
||||||
@singleton
|
@singleton
|
||||||
EncryptedSharedPreferences get encryptedSharedPreferences => EncryptedSharedPreferences();
|
EncryptedSharedPreferences get encryptedSharedPreferences =>
|
||||||
|
EncryptedSharedPreferences();
|
||||||
@singleton
|
@singleton
|
||||||
SecurityContext get securityContext => SecurityContext();
|
SecurityContext get securityContext => SecurityContext();
|
||||||
@singleton
|
@singleton
|
||||||
@@ -28,7 +29,8 @@ abstract class RegisterModule {
|
|||||||
/// Factory method creating an [HttpClient] with the currently registered [SecurityContext].
|
/// Factory method creating an [HttpClient] with the currently registered [SecurityContext].
|
||||||
///
|
///
|
||||||
HttpClient getHttpClient(SecurityContext securityContext) =>
|
HttpClient getHttpClient(SecurityContext securityContext) =>
|
||||||
HttpClient(context: securityContext)..connectionTimeout = const Duration(seconds: 10);
|
HttpClient(context: securityContext)
|
||||||
|
..connectionTimeout = const Duration(seconds: 10);
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient].
|
/// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient].
|
||||||
@@ -50,6 +52,6 @@ abstract class RegisterModule {
|
|||||||
client: IOClient(client),
|
client: IOClient(client),
|
||||||
);
|
);
|
||||||
|
|
||||||
CacheManager getCacheManager(BaseClient client) =>
|
CacheManager getCacheManager(BaseClient client) => CacheManager(
|
||||||
CacheManager(Config('cacheKey', fileService: HttpFileService(httpClient: client)));
|
Config('cacheKey', fileService: HttpFileService(httpClient: client)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:introduction_screen/introduction_screen.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/app_intro/widgets/biometric_authentication_intro_slide.dart';
|
|
||||||
import 'package:paperless_mobile/features/app_intro/widgets/configuration_done_intro_slide.dart';
|
|
||||||
import 'package:paperless_mobile/features/app_intro/widgets/welcome_intro_slide.dart';
|
|
||||||
import 'package:paperless_mobile/features/home/view/home_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
import 'package:intro_slider/intro_slider.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/biometric_authentication_setting.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setting.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
class ApplicationIntroSlideshow extends StatelessWidget {
|
class ApplicationIntroSlideshow extends StatelessWidget {
|
||||||
const ApplicationIntroSlideshow({super.key});
|
const ApplicationIntroSlideshow({super.key});
|
||||||
@@ -16,24 +15,82 @@ class ApplicationIntroSlideshow extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async => false,
|
onWillPop: () async => false,
|
||||||
child: IntroSlider(
|
child: BlocProvider.value(
|
||||||
renderDoneBtn: TextButton(
|
value: getIt<ApplicationSettingsCubit>(),
|
||||||
child: Text("GO"), //TODO: INTL
|
child: IntroductionScreen(
|
||||||
onPressed: () {
|
globalBackgroundColor: Theme.of(context).canvasColor,
|
||||||
Navigator.pop(context);
|
showDoneButton: true,
|
||||||
},
|
next: Text(S.of(context).onboardingNextButtonLabel),
|
||||||
),
|
done: Text(S.of(context).onboardingDoneButtonLabel),
|
||||||
backgroundColorAllTabs: Theme.of(context).canvasColor,
|
onDone: () => Navigator.pop(context),
|
||||||
onDonePress: () => Navigator.of(context)
|
dotsDecorator: DotsDecorator(
|
||||||
.pushReplacement(MaterialPageRoute(builder: (context) => const HomePage())),
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
listCustomTabs: [
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
const WelcomeIntroSlide(),
|
activeSize: Size(16.0, 8.0),
|
||||||
BlocProvider.value(
|
activeShape: RoundedRectangleBorder(
|
||||||
value: getIt<ApplicationSettingsCubit>(),
|
borderRadius: BorderRadius.all(Radius.circular(25.0)),
|
||||||
child: const BiometricAuthenticationIntroSlide(),
|
),
|
||||||
),
|
),
|
||||||
const ConfigurationDoneIntroSlide(),
|
pages: [
|
||||||
].padded(const EdgeInsets.all(16.0)),
|
PageViewModel(
|
||||||
|
titleWidget: Text(
|
||||||
|
"Always right at your fingertip",
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
image: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Image.asset("assets/images/organize_documents.png"),
|
||||||
|
),
|
||||||
|
bodyWidget: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Organizing documents was never this easy",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PageViewModel(
|
||||||
|
titleWidget: Text(
|
||||||
|
"Accessible only by you",
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
image: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Image.asset("assets/images/secure_documents.png"),
|
||||||
|
),
|
||||||
|
bodyWidget: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Secure your documents with biometric authentication and client certificates",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PageViewModel(
|
||||||
|
titleWidget: Text(
|
||||||
|
"You're almost done",
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
image: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Image.asset("assets/images/success.png"),
|
||||||
|
),
|
||||||
|
bodyWidget: Column(
|
||||||
|
children: const [
|
||||||
|
BiometricAuthenticationSetting(),
|
||||||
|
LanguageSelectionSetting(),
|
||||||
|
ThemeModeSetting(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ class BiometricAuthenticationIntroSlide extends StatefulWidget {
|
|||||||
_BiometricAuthenticationIntroSlideState();
|
_BiometricAuthenticationIntroSlideState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BiometricAuthenticationIntroSlideState extends State<BiometricAuthenticationIntroSlide> {
|
class _BiometricAuthenticationIntroSlideState
|
||||||
|
extends State<BiometricAuthenticationIntroSlide> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
//TODO: INTL
|
//TODO: INTL
|
||||||
@@ -58,9 +59,12 @@ class _BiometricAuthenticationIntroSlideState extends State<BiometricAuthenticat
|
|||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
child: Text("Enable"),
|
child: Text("Enable"),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final settings = BlocProvider.of<ApplicationSettingsCubit>(context).state;
|
final settings =
|
||||||
|
BlocProvider.of<ApplicationSettingsCubit>(context)
|
||||||
|
.state;
|
||||||
getIt<AuthenticationService>()
|
getIt<AuthenticationService>()
|
||||||
.authenticateLocalUser("Please authenticate to secure Paperless Mobile")
|
.authenticateLocalUser(
|
||||||
|
"Please authenticate to secure Paperless Mobile")
|
||||||
.then((isEnabled) {
|
.then((isEnabled) {
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
showSnackBar(context,
|
showSnackBar(context,
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
|
||||||
|
|
||||||
class WelcomeIntroSlide extends StatelessWidget {
|
class WelcomeIntroSlide extends StatelessWidget {
|
||||||
const WelcomeIntroSlide({super.key});
|
const WelcomeIntroSlide({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
//TODO: INTL
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Welcome to Paperless Mobile!",
|
"Welcome to Paperless Mobile!",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
Text(
|
Padding(
|
||||||
"Manage and add your documents on the go!",
|
padding: const EdgeInsets.all(16),
|
||||||
textAlign: TextAlign.center,
|
child: Text(
|
||||||
|
"Manage, share and create documents on the go without any compromises!",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Theme.of(context).hintColor),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
Align(child: Image.asset("assets/logos/paperless_logo_green.png")),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
||||||
@@ -11,8 +13,10 @@ import 'package:injectable/injectable.dart';
|
|||||||
@singleton
|
@singleton
|
||||||
class DocumentsCubit extends Cubit<DocumentsState> {
|
class DocumentsCubit extends Cubit<DocumentsState> {
|
||||||
final DocumentRepository documentRepository;
|
final DocumentRepository documentRepository;
|
||||||
|
final GlobalErrorCubit errorCubit;
|
||||||
|
|
||||||
DocumentsCubit(this.documentRepository) : super(DocumentsState.initial);
|
DocumentsCubit(this.documentRepository, this.errorCubit)
|
||||||
|
: super(DocumentsState.initial);
|
||||||
|
|
||||||
Future<void> addDocument(
|
Future<void> addDocument(
|
||||||
Uint8List bytes,
|
Uint8List bytes,
|
||||||
@@ -23,101 +27,217 @@ class DocumentsCubit extends Cubit<DocumentsState> {
|
|||||||
int? correspondent,
|
int? correspondent,
|
||||||
List<int>? tags,
|
List<int>? tags,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
|
bool propagateEventOnError = true,
|
||||||
}) async {
|
}) async {
|
||||||
await documentRepository.create(
|
try {
|
||||||
bytes,
|
await documentRepository.create(
|
||||||
fileName,
|
bytes,
|
||||||
title: title,
|
fileName,
|
||||||
documentType: documentType,
|
title: title,
|
||||||
correspondent: correspondent,
|
documentType: documentType,
|
||||||
tags: tags,
|
correspondent: correspondent,
|
||||||
createdAt: createdAt,
|
tags: tags,
|
||||||
);
|
createdAt: createdAt,
|
||||||
|
);
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
// documentRepository
|
// documentRepository
|
||||||
// .waitForConsumptionFinished(fileName, title)
|
// .waitForConsumptionFinished(fileName, title)
|
||||||
// .then((value) => onConsumptionFinished(value));
|
// .then((value) => onConsumptionFinished(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeDocument(DocumentModel document) async {
|
Future<void> removeDocument(
|
||||||
await documentRepository.delete(document);
|
DocumentModel document, {
|
||||||
return await reloadDocuments();
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await documentRepository.delete(document);
|
||||||
|
return await reloadDocuments();
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> bulkRemoveDocuments(List<DocumentModel> documents) async {
|
Future<void> bulkRemoveDocuments(List<DocumentModel> documents,
|
||||||
await documentRepository.bulkDelete(documents);
|
{bool propagateEventOnError = true}) async {
|
||||||
return await reloadDocuments();
|
try {
|
||||||
|
await documentRepository.bulkDelete(documents);
|
||||||
|
return await reloadDocuments();
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateDocument(DocumentModel document) async {
|
Future<void> updateDocument(
|
||||||
await documentRepository.update(document);
|
DocumentModel document, {
|
||||||
await reloadDocuments();
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await documentRepository.update(document);
|
||||||
|
await reloadDocuments();
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadDocuments() async {
|
Future<void> loadDocuments({
|
||||||
final result = await documentRepository.find(state.filter);
|
bool propagateEventOnError = true,
|
||||||
emit(DocumentsState(
|
}) async {
|
||||||
isLoaded: true,
|
try {
|
||||||
value: [...state.value, result],
|
final result = await documentRepository.find(state.filter);
|
||||||
filter: state.filter,
|
emit(DocumentsState(
|
||||||
));
|
isLoaded: true,
|
||||||
|
value: [...state.value, result],
|
||||||
|
filter: state.filter,
|
||||||
|
));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reloadDocuments() async {
|
Future<void> reloadDocuments({
|
||||||
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
if (state.currentPageNumber >= 5) {
|
if (state.currentPageNumber >= 5) {
|
||||||
return _bulkReloadDocuments();
|
return _bulkReloadDocuments();
|
||||||
}
|
}
|
||||||
var newPages = <PagedSearchResult>[];
|
var newPages = <PagedSearchResult>[];
|
||||||
for (final page in state.value) {
|
try {
|
||||||
final result = await documentRepository.find(state.filter.copyWith(page: page.pageKey));
|
for (final page in state.value) {
|
||||||
newPages.add(result);
|
final result = await documentRepository
|
||||||
|
.find(state.filter.copyWith(page: page.pageKey));
|
||||||
|
newPages.add(result);
|
||||||
|
}
|
||||||
|
emit(DocumentsState(
|
||||||
|
isLoaded: true, value: newPages, filter: state.filter));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emit(DocumentsState(isLoaded: true, value: newPages, filter: state.filter));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _bulkReloadDocuments() async {
|
Future<void> _bulkReloadDocuments({
|
||||||
final result = await documentRepository
|
bool propagateEventOnError = true,
|
||||||
.find(state.filter.copyWith(page: 1, pageSize: state.documents.length));
|
}) async {
|
||||||
emit(DocumentsState(isLoaded: true, value: [result], filter: state.filter));
|
try {
|
||||||
|
final result = await documentRepository.find(
|
||||||
|
state.filter.copyWith(page: 1, pageSize: state.documents.length));
|
||||||
|
emit(DocumentsState(
|
||||||
|
isLoaded: true, value: [result], filter: state.filter));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadMore() async {
|
Future<void> loadMore({
|
||||||
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
if (state.isLastPageLoaded) {
|
if (state.isLastPageLoaded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
|
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
|
||||||
final result = await documentRepository.find(newFilter);
|
try {
|
||||||
emit(DocumentsState(isLoaded: true, value: [...state.value, result], filter: newFilter));
|
final result = await documentRepository.find(newFilter);
|
||||||
|
emit(DocumentsState(
|
||||||
|
isLoaded: true, value: [...state.value, result], filter: newFilter));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> assignAsn(DocumentModel document) async {
|
Future<void> assignAsn(
|
||||||
if (document.archiveSerialNumber == null) {
|
DocumentModel document, {
|
||||||
final int asn = await documentRepository.findNextAsn();
|
bool propagateEventOnError = true,
|
||||||
updateDocument(document.copyWith(archiveSerialNumber: asn));
|
}) async {
|
||||||
|
try {
|
||||||
|
if (document.archiveSerialNumber == null) {
|
||||||
|
final int asn = await documentRepository.findNextAsn();
|
||||||
|
updateDocument(document.copyWith(archiveSerialNumber: asn));
|
||||||
|
}
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Update filter state and automatically reload documents. Always resets page to 1.
|
/// Update filter state and automatically reload documents. Always resets page to 1.
|
||||||
/// Use [DocumentsCubit.loadMore] to load more data.
|
/// Use [DocumentsCubit.loadMore] to load more data.
|
||||||
Future<void> updateFilter({
|
Future<void> updateFilter(
|
||||||
final DocumentFilter filter = DocumentFilter.initial,
|
{final DocumentFilter filter = DocumentFilter.initial,
|
||||||
}) async {
|
bool propagateEventOnError = true}) async {
|
||||||
final result = await documentRepository.find(filter.copyWith(page: 1));
|
try {
|
||||||
emit(DocumentsState(filter: filter, value: [result], isLoaded: true));
|
final result = await documentRepository.find(filter.copyWith(page: 1));
|
||||||
|
emit(DocumentsState(filter: filter, value: [result], isLoaded: true));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter.
|
/// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter.
|
||||||
///
|
///
|
||||||
Future<void> updateCurrentFilter(final DocumentFilter Function(DocumentFilter) transformFn) {
|
Future<void> updateCurrentFilter(
|
||||||
return updateFilter(filter: transformFn(state.filter));
|
final DocumentFilter Function(DocumentFilter) transformFn, {
|
||||||
|
bool propagateEventOnError = true,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return updateFilter(filter: transformFn(state.filter));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
return errorCubit.add(error);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleDocumentSelection(DocumentModel model) {
|
void toggleDocumentSelection(DocumentModel model) {
|
||||||
if (state.selection.contains(model)) {
|
if (state.selection.contains(model)) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
selection: state.selection.where((element) => element.id != model.id).toList(),
|
selection: state.selection
|
||||||
|
.where((element) => element.id != model.id)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ class DocumentsState extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<DocumentModel> get documents {
|
List<DocumentModel> get documents {
|
||||||
return value.fold([], (previousValue, element) => [...previousValue, ...element.results]);
|
return value.fold(
|
||||||
|
[], (previousValue, element) => [...previousValue, ...element.results]);
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentsState copyWith({
|
DocumentsState copyWith({
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/saved_view_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/saved_view_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/saved_view.model.dart';
|
import 'package:paperless_mobile/features/documents/model/saved_view.model.dart';
|
||||||
@@ -7,41 +9,79 @@ import 'package:injectable/injectable.dart';
|
|||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class SavedViewCubit extends Cubit<SavedViewState> {
|
class SavedViewCubit extends Cubit<SavedViewState> {
|
||||||
SavedViewCubit() : super(SavedViewState(value: {}));
|
final GlobalErrorCubit errorCubit;
|
||||||
|
SavedViewCubit(this.errorCubit) : super(SavedViewState(value: {}));
|
||||||
|
|
||||||
void selectView(SavedView? view) {
|
void selectView(SavedView? view, {bool propagateEventOnError = true}) {
|
||||||
emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id));
|
try {
|
||||||
|
emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SavedView> add(SavedView view) async {
|
Future<SavedView> add(
|
||||||
final savedView = await getIt<SavedViewsRepository>().save(view);
|
SavedView view, {
|
||||||
emit(
|
bool propagateEventOnError = true,
|
||||||
SavedViewState(
|
}) async {
|
||||||
value: {...state.value, savedView.id!: savedView},
|
try {
|
||||||
selectedSavedViewId: state.selectedSavedViewId,
|
final savedView = await getIt<SavedViewsRepository>().save(view);
|
||||||
),
|
emit(
|
||||||
);
|
SavedViewState(
|
||||||
return savedView;
|
value: {...state.value, savedView.id!: savedView},
|
||||||
|
selectedSavedViewId: state.selectedSavedViewId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return savedView;
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> remove(SavedView view) async {
|
Future<int> remove(
|
||||||
final id = await getIt<SavedViewsRepository>().delete(view);
|
SavedView view, {
|
||||||
final newValue = {...state.value};
|
bool propagateEventOnError = true,
|
||||||
newValue.removeWhere((key, value) => key == id);
|
}) async {
|
||||||
emit(
|
try {
|
||||||
SavedViewState(
|
final id = await getIt<SavedViewsRepository>().delete(view);
|
||||||
value: newValue,
|
final newValue = {...state.value};
|
||||||
selectedSavedViewId:
|
newValue.removeWhere((key, value) => key == id);
|
||||||
view.id == state.selectedSavedViewId ? null : state.selectedSavedViewId,
|
emit(
|
||||||
),
|
SavedViewState(
|
||||||
);
|
value: newValue,
|
||||||
return id;
|
selectedSavedViewId: view.id == state.selectedSavedViewId
|
||||||
|
? null
|
||||||
|
: state.selectedSavedViewId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return id;
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize({
|
||||||
final views = await getIt<SavedViewsRepository>().getAll();
|
bool propagateEventOnError = true,
|
||||||
final values = {for (var element in views) element.id!: element};
|
}) async {
|
||||||
emit(SavedViewState(value: values));
|
try {
|
||||||
|
final views = await getIt<SavedViewsRepository>().getAll();
|
||||||
|
final values = {for (var element in views) element.id!: element};
|
||||||
|
emit(SavedViewState(value: values));
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetSelection() {
|
void resetSelection() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
|
|
||||||
class BulkEditAction {
|
class BulkEditAction {
|
||||||
final List<int> documents;
|
final List<int> documents;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ignore_for_file: non_constant_identifier_names
|
// ignore_for_file: non_constant_identifier_names
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/query_parameters/id_query_parameter.dart';
|
import 'package:paperless_mobile/features/documents/model/query_parameters/id_query_parameter.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -70,19 +70,23 @@ class DocumentFilter with EquatableMixin {
|
|||||||
|
|
||||||
// Add/subtract one day in the following because paperless uses gt/lt not gte/lte
|
// Add/subtract one day in the following because paperless uses gt/lt not gte/lte
|
||||||
if (addedDateAfter != null) {
|
if (addedDateAfter != null) {
|
||||||
sb.write("&added__date__gt=${dateFormat.format(addedDateAfter!.subtract(_oneDay))}");
|
sb.write(
|
||||||
|
"&added__date__gt=${dateFormat.format(addedDateAfter!.subtract(_oneDay))}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addedDateBefore != null) {
|
if (addedDateBefore != null) {
|
||||||
sb.write("&added__date__lt=${dateFormat.format(addedDateBefore!.add(_oneDay))}");
|
sb.write(
|
||||||
|
"&added__date__lt=${dateFormat.format(addedDateBefore!.add(_oneDay))}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createdDateAfter != null) {
|
if (createdDateAfter != null) {
|
||||||
sb.write("&created__date__gt=${dateFormat.format(createdDateAfter!.subtract(_oneDay))}");
|
sb.write(
|
||||||
|
"&created__date__gt=${dateFormat.format(createdDateAfter!.subtract(_oneDay))}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createdDateBefore != null) {
|
if (createdDateBefore != null) {
|
||||||
sb.write("&created__date__lt=${dateFormat.format(createdDateBefore!.add(_oneDay))}");
|
sb.write(
|
||||||
|
"&created__date__lt=${dateFormat.format(createdDateBefore!.add(_oneDay))}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
|
import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
|
import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
|
||||||
@@ -83,15 +83,20 @@ class FilterRule with EquatableMixin {
|
|||||||
: TagsQuery.fromIds([...filter.tags.ids, int.parse(value!)]),
|
: TagsQuery.fromIds([...filter.tags.ids, int.parse(value!)]),
|
||||||
);
|
);
|
||||||
case createdBeforeRule:
|
case createdBeforeRule:
|
||||||
return filter.copyWith(createdDateBefore: value == null ? null : DateTime.parse(value!));
|
return filter.copyWith(
|
||||||
|
createdDateBefore: value == null ? null : DateTime.parse(value!));
|
||||||
case createdAfterRule:
|
case createdAfterRule:
|
||||||
return filter.copyWith(createdDateAfter: value == null ? null : DateTime.parse(value!));
|
return filter.copyWith(
|
||||||
|
createdDateAfter: value == null ? null : DateTime.parse(value!));
|
||||||
case addedBeforeRule:
|
case addedBeforeRule:
|
||||||
return filter.copyWith(addedDateBefore: value == null ? null : DateTime.parse(value!));
|
return filter.copyWith(
|
||||||
|
addedDateBefore: value == null ? null : DateTime.parse(value!));
|
||||||
case addedAfterRule:
|
case addedAfterRule:
|
||||||
return filter.copyWith(addedDateAfter: value == null ? null : DateTime.parse(value!));
|
return filter.copyWith(
|
||||||
|
addedDateAfter: value == null ? null : DateTime.parse(value!));
|
||||||
case titleAndContentRule:
|
case titleAndContentRule:
|
||||||
return filter.copyWith(queryText: value, queryType: QueryType.titleAndContent);
|
return filter.copyWith(
|
||||||
|
queryText: value, queryType: QueryType.titleAndContent);
|
||||||
case extendedRule:
|
case extendedRule:
|
||||||
return filter.copyWith(queryText: value, queryType: QueryType.extended);
|
return filter.copyWith(queryText: value, queryType: QueryType.extended);
|
||||||
//TODO: Add currently unused rules
|
//TODO: Add currently unused rules
|
||||||
@@ -109,25 +114,29 @@ class FilterRule with EquatableMixin {
|
|||||||
filterRules.add(FilterRule(correspondentRule, null));
|
filterRules.add(FilterRule(correspondentRule, null));
|
||||||
}
|
}
|
||||||
if (filter.correspondent.isSet) {
|
if (filter.correspondent.isSet) {
|
||||||
filterRules.add(FilterRule(correspondentRule, filter.correspondent.id!.toString()));
|
filterRules.add(
|
||||||
|
FilterRule(correspondentRule, filter.correspondent.id!.toString()));
|
||||||
}
|
}
|
||||||
if (filter.documentType.onlyNotAssigned) {
|
if (filter.documentType.onlyNotAssigned) {
|
||||||
filterRules.add(FilterRule(documentTypeRule, null));
|
filterRules.add(FilterRule(documentTypeRule, null));
|
||||||
}
|
}
|
||||||
if (filter.documentType.isSet) {
|
if (filter.documentType.isSet) {
|
||||||
filterRules.add(FilterRule(documentTypeRule, filter.documentType.id!.toString()));
|
filterRules.add(
|
||||||
|
FilterRule(documentTypeRule, filter.documentType.id!.toString()));
|
||||||
}
|
}
|
||||||
if (filter.storagePath.onlyNotAssigned) {
|
if (filter.storagePath.onlyNotAssigned) {
|
||||||
filterRules.add(FilterRule(storagePathRule, null));
|
filterRules.add(FilterRule(storagePathRule, null));
|
||||||
}
|
}
|
||||||
if (filter.storagePath.isSet) {
|
if (filter.storagePath.isSet) {
|
||||||
filterRules.add(FilterRule(storagePathRule, filter.storagePath.id!.toString()));
|
filterRules
|
||||||
|
.add(FilterRule(storagePathRule, filter.storagePath.id!.toString()));
|
||||||
}
|
}
|
||||||
if (filter.tags.onlyNotAssigned) {
|
if (filter.tags.onlyNotAssigned) {
|
||||||
filterRules.add(FilterRule(tagRule, null));
|
filterRules.add(FilterRule(tagRule, null));
|
||||||
}
|
}
|
||||||
if (filter.tags.isSet) {
|
if (filter.tags.isSet) {
|
||||||
filterRules.addAll(filter.tags.ids.map((id) => FilterRule(tagRule, id.toString())));
|
filterRules.addAll(
|
||||||
|
filter.tags.ids.map((id) => FilterRule(tagRule, id.toString())));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.queryText != null) {
|
if (filter.queryText != null) {
|
||||||
@@ -147,16 +156,20 @@ class FilterRule with EquatableMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filter.createdDateAfter != null) {
|
if (filter.createdDateAfter != null) {
|
||||||
filterRules.add(FilterRule(createdAfterRule, dateFormat.format(filter.createdDateAfter!)));
|
filterRules.add(FilterRule(
|
||||||
|
createdAfterRule, dateFormat.format(filter.createdDateAfter!)));
|
||||||
}
|
}
|
||||||
if (filter.createdDateBefore != null) {
|
if (filter.createdDateBefore != null) {
|
||||||
filterRules.add(FilterRule(createdBeforeRule, dateFormat.format(filter.createdDateBefore!)));
|
filterRules.add(FilterRule(
|
||||||
|
createdBeforeRule, dateFormat.format(filter.createdDateBefore!)));
|
||||||
}
|
}
|
||||||
if (filter.addedDateAfter != null) {
|
if (filter.addedDateAfter != null) {
|
||||||
filterRules.add(FilterRule(addedAfterRule, dateFormat.format(filter.addedDateAfter!)));
|
filterRules.add(FilterRule(
|
||||||
|
addedAfterRule, dateFormat.format(filter.addedDateAfter!)));
|
||||||
}
|
}
|
||||||
if (filter.addedDateBefore != null) {
|
if (filter.addedDateBefore != null) {
|
||||||
filterRules.add(FilterRule(addedBeforeRule, dateFormat.format(filter.addedDateBefore!)));
|
filterRules.add(FilterRule(
|
||||||
|
addedBeforeRule, dateFormat.format(filter.addedDateBefore!)));
|
||||||
}
|
}
|
||||||
return filterRules;
|
return filterRules;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
||||||
|
|
||||||
const pageRegex = r".*page=(\d+).*";
|
const pageRegex = r".*page=(\d+).*";
|
||||||
@@ -45,7 +45,8 @@ class PagedSearchResult<T> extends Equatable {
|
|||||||
required this.results,
|
required this.results,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PagedSearchResult.fromJson(Map<dynamic, dynamic> json, T Function(JSON) fromJson) {
|
factory PagedSearchResult.fromJson(
|
||||||
|
Map<dynamic, dynamic> json, T Function(JSON) fromJson) {
|
||||||
return PagedSearchResult(
|
return PagedSearchResult(
|
||||||
count: json['count'],
|
count: json['count'],
|
||||||
next: json['next'],
|
next: json['next'],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/filter_rule.model.dart';
|
import 'package:paperless_mobile/features/documents/model/filter_rule.model.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_field.dart';
|
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_field.dart';
|
||||||
@@ -33,8 +33,14 @@ class SavedView with EquatableMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props =>
|
List<Object?> get props => [
|
||||||
[name, showOnDashboard, showInSidebar, sortField, sortReverse, filterRules];
|
name,
|
||||||
|
showOnDashboard,
|
||||||
|
showInSidebar,
|
||||||
|
sortField,
|
||||||
|
sortReverse,
|
||||||
|
filterRules
|
||||||
|
];
|
||||||
|
|
||||||
SavedView.fromJson(JSON json)
|
SavedView.fromJson(JSON json)
|
||||||
: this(
|
: this(
|
||||||
@@ -42,11 +48,14 @@ class SavedView with EquatableMixin {
|
|||||||
name: json['name'],
|
name: json['name'],
|
||||||
showOnDashboard: json['show_on_dashboard'],
|
showOnDashboard: json['show_on_dashboard'],
|
||||||
showInSidebar: json['show_in_sidebar'],
|
showInSidebar: json['show_in_sidebar'],
|
||||||
sortField:
|
sortField: SortField.values
|
||||||
SortField.values.where((order) => order.queryString == json['sort_field']).first,
|
.where((order) => order.queryString == json['sort_field'])
|
||||||
|
.first,
|
||||||
sortReverse: json['sort_reverse'],
|
sortReverse: json['sort_reverse'],
|
||||||
filterRules:
|
filterRules: json['filter_rules']
|
||||||
json['filter_rules'].cast<JSON>().map<FilterRule>(FilterRule.fromJson).toList(),
|
.cast<JSON>()
|
||||||
|
.map<FilterRule>(FilterRule.fromJson)
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
DocumentFilter toDocumentFilter() {
|
DocumentFilter toDocumentFilter() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
||||||
|
|
||||||
class SimilarDocumentModel extends DocumentModel {
|
class SimilarDocumentModel extends DocumentModel {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ abstract class DocumentRepository {
|
|||||||
Future<List<int>> bulkDelete(List<DocumentModel> models);
|
Future<List<int>> bulkDelete(List<DocumentModel> models);
|
||||||
Future<Uint8List> getPreview(int docId);
|
Future<Uint8List> getPreview(int docId);
|
||||||
String getThumbnailUrl(int docId);
|
String getThumbnailUrl(int docId);
|
||||||
Future<DocumentModel> waitForConsumptionFinished(String filename, String title);
|
Future<DocumentModel> waitForConsumptionFinished(
|
||||||
|
String filename, String title);
|
||||||
Future<Uint8List> download(DocumentModel document);
|
Future<Uint8List> download(DocumentModel document);
|
||||||
|
|
||||||
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/core/util.dart';
|
import 'package:paperless_mobile/core/util.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
@@ -69,10 +69,10 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
|
|
||||||
fields.tryPutIfAbsent('title', () => title);
|
fields.tryPutIfAbsent('title', () => title);
|
||||||
fields.tryPutIfAbsent('created', () => formatDateNullable(createdAt));
|
fields.tryPutIfAbsent('created', () => formatDateNullable(createdAt));
|
||||||
fields.tryPutIfAbsent(
|
fields.tryPutIfAbsent('correspondent',
|
||||||
'correspondent', () => correspondent == null ? null : json.encode(correspondent));
|
() => correspondent == null ? null : json.encode(correspondent));
|
||||||
fields.tryPutIfAbsent(
|
fields.tryPutIfAbsent('document_type',
|
||||||
'document_type', () => documentType == null ? null : json.encode(documentType));
|
() => documentType == null ? null : json.encode(documentType));
|
||||||
|
|
||||||
for (final key in fields.keys) {
|
for (final key in fields.keys) {
|
||||||
bodyBuffer.write(_buildMultipartField(key, fields[key]!, boundary));
|
bodyBuffer.write(_buildMultipartField(key, fields[key]!, boundary));
|
||||||
@@ -90,7 +90,8 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
final closing = "\r\n--" + boundary + "--\r\n";
|
final closing = "\r\n--" + boundary + "--\r\n";
|
||||||
|
|
||||||
// Set headers
|
// Set headers
|
||||||
request.headers.set(HttpHeaders.contentTypeHeader, "multipart/form-data; boundary=" + boundary);
|
request.headers.set(HttpHeaders.contentTypeHeader,
|
||||||
|
"multipart/form-data; boundary=" + boundary);
|
||||||
request.headers.set(HttpHeaders.contentLengthHeader,
|
request.headers.set(HttpHeaders.contentLengthHeader,
|
||||||
"${bodyBuffer.length + closing.length + documentBytes.lengthInBytes}");
|
"${bodyBuffer.length + closing.length + documentBytes.lengthInBytes}");
|
||||||
request.headers.set(HttpHeaders.authorizationHeader, "Token ${auth.token}");
|
request.headers.set(HttpHeaders.authorizationHeader, "Token ${auth.token}");
|
||||||
@@ -105,7 +106,8 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
final response = await request.close();
|
final response = await request.close();
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw ErrorMessage(ErrorCode.documentUploadFailed, httpStatusCode: response.statusCode);
|
throw ErrorMessage(ErrorCode.documentUploadFailed,
|
||||||
|
httpStatusCode: response.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,19 +123,23 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
String _boundaryString() {
|
String _boundaryString() {
|
||||||
Random _random = Random();
|
Random _random = Random();
|
||||||
var prefix = 'dart-http-boundary-';
|
var prefix = 'dart-http-boundary-';
|
||||||
var list = List<int>.generate(70 - prefix.length,
|
var list = List<int>.generate(
|
||||||
(index) => boundaryCharacters[_random.nextInt(boundaryCharacters.length)],
|
70 - prefix.length,
|
||||||
|
(index) =>
|
||||||
|
boundaryCharacters[_random.nextInt(boundaryCharacters.length)],
|
||||||
growable: false);
|
growable: false);
|
||||||
return '$prefix${String.fromCharCodes(list)}';
|
return '$prefix${String.fromCharCodes(list)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DocumentModel> update(DocumentModel doc) async {
|
Future<DocumentModel> update(DocumentModel doc) async {
|
||||||
final response = await httpClient.put(Uri.parse("/api/documents/${doc.id}/"),
|
final response = await httpClient.put(
|
||||||
|
Uri.parse("/api/documents/${doc.id}/"),
|
||||||
body: json.encode(doc.toJson()),
|
body: json.encode(doc.toJson()),
|
||||||
headers: {"Content-Type": "application/json"}).timeout(requestTimeout);
|
headers: {"Content-Type": "application/json"}).timeout(requestTimeout);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return DocumentModel.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
return DocumentModel.fromJson(
|
||||||
|
jsonDecode(utf8.decode(response.bodyBytes)));
|
||||||
} else {
|
} else {
|
||||||
throw const ErrorMessage(ErrorCode.documentUpdateFailed);
|
throw const ErrorMessage(ErrorCode.documentUpdateFailed);
|
||||||
}
|
}
|
||||||
@@ -158,7 +164,8 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> delete(DocumentModel doc) async {
|
Future<int> delete(DocumentModel doc) async {
|
||||||
final response = await httpClient.delete(Uri.parse("/api/documents/${doc.id}/"));
|
final response =
|
||||||
|
await httpClient.delete(Uri.parse("/api/documents/${doc.id}/"));
|
||||||
|
|
||||||
if (response.statusCode == 204) {
|
if (response.statusCode == 204) {
|
||||||
return Future.value(doc.id);
|
return Future.value(doc.id);
|
||||||
@@ -221,13 +228,16 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DocumentModel> waitForConsumptionFinished(String fileName, String title) async {
|
Future<DocumentModel> waitForConsumptionFinished(
|
||||||
|
String fileName, String title) async {
|
||||||
// Always wait 5 seconds, processing usually takes longer...
|
// Always wait 5 seconds, processing usually takes longer...
|
||||||
//await Future.delayed(const Duration(seconds: 5));
|
//await Future.delayed(const Duration(seconds: 5));
|
||||||
PagedSearchResult<DocumentModel> results = await find(DocumentFilter.latestDocument);
|
PagedSearchResult<DocumentModel> results =
|
||||||
|
await find(DocumentFilter.latestDocument);
|
||||||
|
|
||||||
while ((results.results.isEmpty ||
|
while ((results.results.isEmpty ||
|
||||||
(results.results[0].originalFileName != fileName && results.results[0].title != title))) {
|
(results.results[0].originalFileName != fileName &&
|
||||||
|
results.results[0].title != title))) {
|
||||||
//TODO: maybe implement more intelligent retry logic or find workaround for websocket authentication...
|
//TODO: maybe implement more intelligent retry logic or find workaround for websocket authentication...
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
results = await find(DocumentFilter.latestDocument);
|
results = await find(DocumentFilter.latestDocument);
|
||||||
@@ -242,20 +252,23 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Uint8List> download(DocumentModel document) async {
|
Future<Uint8List> download(DocumentModel document) async {
|
||||||
//TODO: Check if this works...
|
//TODO: Check if this works...
|
||||||
final response = await httpClient.get(Uri.parse("/api/documents/${document.id}/download/"));
|
final response = await httpClient
|
||||||
|
.get(Uri.parse("/api/documents/${document.id}/download/"));
|
||||||
return response.bodyBytes;
|
return response.bodyBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
|
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
|
||||||
final response = await httpClient.get(Uri.parse("/api/documents/${document.id}/metadata/"));
|
final response = await httpClient
|
||||||
return DocumentMetaData.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
.get(Uri.parse("/api/documents/${document.id}/metadata/"));
|
||||||
|
return DocumentMetaData.fromJson(
|
||||||
|
jsonDecode(utf8.decode(response.bodyBytes)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> autocomplete(String query, [int limit = 10]) async {
|
Future<List<String>> autocomplete(String query, [int limit = 10]) async {
|
||||||
final response =
|
final response = await httpClient
|
||||||
await httpClient.get(Uri.parse("/api/search/autocomplete/?query=$query&limit=$limit}"));
|
.get(Uri.parse("/api/search/autocomplete/?query=$query&limit=$limit}"));
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return jsonDecode(utf8.decode(response.bodyBytes)) as List<String>;
|
return jsonDecode(utf8.decode(response.bodyBytes)) as List<String>;
|
||||||
}
|
}
|
||||||
@@ -264,8 +277,8 @@ class DocumentRepositoryImpl implements DocumentRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<SimilarDocumentModel>> findSimilar(int docId) async {
|
Future<List<SimilarDocumentModel>> findSimilar(int docId) async {
|
||||||
final response =
|
final response = await httpClient
|
||||||
await httpClient.get(Uri.parse("/api/documents/?more_like=$docId&pageSize=10"));
|
.get(Uri.parse("/api/documents/?more_like=$docId&pageSize=10"));
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return PagedSearchResult<SimilarDocumentModel>.fromJson(
|
return PagedSearchResult<SimilarDocumentModel>.fromJson(
|
||||||
jsonDecode(utf8.decode(response.bodyBytes)),
|
jsonDecode(utf8.decode(response.bodyBytes)),
|
||||||
|
|||||||
@@ -39,15 +39,18 @@ class SavedViewRepositoryImpl implements SavedViewsRepository {
|
|||||||
if (response.statusCode == 201) {
|
if (response.statusCode == 201) {
|
||||||
return SavedView.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
return SavedView.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
||||||
}
|
}
|
||||||
throw ErrorMessage(ErrorCode.createSavedViewError, httpStatusCode: response.statusCode);
|
throw ErrorMessage(ErrorCode.createSavedViewError,
|
||||||
|
httpStatusCode: response.statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> delete(SavedView view) async {
|
Future<int> delete(SavedView view) async {
|
||||||
final response = await httpClient.delete(Uri.parse("/api/saved_views/${view.id}/"));
|
final response =
|
||||||
|
await httpClient.delete(Uri.parse("/api/saved_views/${view.id}/"));
|
||||||
if (response.statusCode == 204) {
|
if (response.statusCode == 204) {
|
||||||
return view.id!;
|
return view.id!;
|
||||||
}
|
}
|
||||||
throw ErrorMessage(ErrorCode.deleteSavedViewError, httpStatusCode: response.statusCode);
|
throw ErrorMessage(ErrorCode.deleteSavedViewError,
|
||||||
|
httpStatusCode: response.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,14 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
|
|
||||||
class DocumentDetailsPage extends StatefulWidget {
|
class DocumentDetailsPage extends StatefulWidget {
|
||||||
final int documentId;
|
final int documentId;
|
||||||
|
final bool allowEdit;
|
||||||
|
final bool isLabelClickable;
|
||||||
|
|
||||||
const DocumentDetailsPage({
|
const DocumentDetailsPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.documentId,
|
required this.documentId,
|
||||||
|
this.allowEdit = true,
|
||||||
|
this.isLabelClickable = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,7 +47,8 @@ class DocumentDetailsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||||
static final DateFormat _detailedDateFormat = DateFormat("MMM d, yyyy HH:mm:ss");
|
static final DateFormat _detailedDateFormat =
|
||||||
|
DateFormat("MMM d, yyyy HH:mm:ss");
|
||||||
|
|
||||||
bool _isDownloadPending = false;
|
bool _isDownloadPending = false;
|
||||||
bool _isAssignAsnPending = false;
|
bool _isAssignAsnPending = false;
|
||||||
@@ -52,98 +58,112 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
// buildWhen required because rebuild would happen after delete causing error.
|
// buildWhen required because rebuild would happen after delete causing error.
|
||||||
buildWhen: (previous, current) {
|
buildWhen: (previous, current) {
|
||||||
return current.documents.where((element) => element.id == widget.documentId).isNotEmpty;
|
return current.documents
|
||||||
|
.where((element) => element.id == widget.documentId)
|
||||||
|
.isNotEmpty;
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final document = state.documents.where((doc) => doc.id == widget.documentId).first;
|
final document =
|
||||||
return SafeArea(
|
state.documents.where((doc) => doc.id == widget.documentId).first;
|
||||||
bottom: true,
|
return DefaultTabController(
|
||||||
child: DefaultTabController(
|
length: 3,
|
||||||
length: 3,
|
child: Scaffold(
|
||||||
child: Scaffold(
|
floatingActionButtonLocation:
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
FloatingActionButtonLocation.endDocked,
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: widget.allowEdit
|
||||||
child: const Icon(Icons.edit),
|
? FloatingActionButton(
|
||||||
onPressed: () => _onEdit(document),
|
child: const Icon(Icons.edit),
|
||||||
),
|
onPressed: () => _onEdit(document),
|
||||||
bottomNavigationBar: BottomAppBar(
|
)
|
||||||
child: Row(
|
: null,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
bottomNavigationBar: BottomAppBar(
|
||||||
children: [
|
child: Row(
|
||||||
IconButton(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
icon: const Icon(Icons.delete),
|
children: [
|
||||||
onPressed: () => _onDelete(document),
|
IconButton(
|
||||||
).padded(const EdgeInsets.symmetric(horizontal: 8.0)),
|
icon: const Icon(Icons.delete),
|
||||||
IconButton(
|
onPressed:
|
||||||
icon: const Icon(Icons.download),
|
widget.allowEdit ? () => _onDelete(document) : null,
|
||||||
onPressed: Platform.isAndroid ? () => _onDownload(document) : null,
|
).padded(const EdgeInsets.symmetric(horizontal: 4)),
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
icon: const Icon(Icons.download),
|
||||||
icon: const Icon(Icons.open_in_new),
|
onPressed:
|
||||||
onPressed: () => _onOpen(document),
|
Platform.isAndroid ? () => _onDownload(document) : null,
|
||||||
).padded(const EdgeInsets.symmetric(horizontal: 8.0)),
|
).padded(const EdgeInsets.only(right: 4)),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.open_in_new),
|
||||||
onPressed: () => _onShare(document),
|
onPressed: () => _onOpen(document),
|
||||||
),
|
).padded(const EdgeInsets.only(right: 4)),
|
||||||
],
|
IconButton(
|
||||||
),
|
icon: const Icon(Icons.share),
|
||||||
),
|
onPressed: () => _onShare(document),
|
||||||
body: NestedScrollView(
|
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
|
||||||
SliverAppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors
|
|
||||||
.black, //TODO: check if there is a way to dynamically determine color...
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
expandedHeight: 200.0,
|
|
||||||
flexibleSpace: DocumentPreview(
|
|
||||||
id: document.id,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
bottom: ColoredTabBar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
tabBar: TabBar(
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
child: Text(
|
|
||||||
S.of(context).documentDetailsPageTabOverviewLabel,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Text(
|
|
||||||
S.of(context).documentDetailsPageTabContentLabel,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Text(
|
|
||||||
S.of(context).documentDetailsPageTabMetaDataLabel,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
body: TabBarView(
|
),
|
||||||
children: [
|
),
|
||||||
_buildDocumentOverview(document, state.filter.titleAndContentMatchString),
|
body: NestedScrollView(
|
||||||
_buildDocumentContentView(document, state.filter.titleAndContentMatchString),
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
_buildDocumentMetaDataView(document),
|
SliverAppBar(
|
||||||
].padded(),
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors
|
||||||
|
.black, //TODO: check if there is a way to dynamically determine color...
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
expandedHeight: 200.0,
|
||||||
|
flexibleSpace: DocumentPreview(
|
||||||
|
id: document.id,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
bottom: ColoredTabBar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
tabBar: TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentDetailsPageTabOverviewLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentDetailsPageTabContentLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentDetailsPageTabMetaDataLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
body: TabBarView(
|
||||||
|
children: [
|
||||||
|
_buildDocumentOverview(
|
||||||
|
document, state.filter.titleAndContentMatchString),
|
||||||
|
_buildDocumentContentView(
|
||||||
|
document, state.filter.titleAndContentMatchString),
|
||||||
|
_buildDocumentMetaDataView(document),
|
||||||
|
].padded(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -163,18 +183,25 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
_DetailsItem.text(_detailedDateFormat.format(document.modified),
|
_DetailsItem.text(_detailedDateFormat.format(document.modified),
|
||||||
label: S.of(context).documentModifiedPropertyLabel, context: context),
|
label: S.of(context).documentModifiedPropertyLabel,
|
||||||
|
context: context),
|
||||||
_separator(),
|
_separator(),
|
||||||
_DetailsItem.text(_detailedDateFormat.format(document.added),
|
_DetailsItem.text(_detailedDateFormat.format(document.added),
|
||||||
label: S.of(context).documentAddedPropertyLabel, context: context),
|
label: S.of(context).documentAddedPropertyLabel,
|
||||||
|
context: context),
|
||||||
_separator(),
|
_separator(),
|
||||||
_DetailsItem(
|
_DetailsItem(
|
||||||
label: S.of(context).documentArchiveSerialNumberPropertyLongLabel,
|
label: S.of(context).documentArchiveSerialNumberPropertyLongLabel,
|
||||||
content: document.archiveSerialNumber != null
|
content: document.archiveSerialNumber != null
|
||||||
? Text(document.archiveSerialNumber.toString())
|
? Text(document.archiveSerialNumber.toString())
|
||||||
: OutlinedButton(
|
: OutlinedButton(
|
||||||
child: Text(S.of(context).documentDetailsPageAssignAsnButtonLabel),
|
child: Text(S
|
||||||
onPressed: () => BlocProvider.of<DocumentsCubit>(context).assignAsn(document),
|
.of(context)
|
||||||
|
.documentDetailsPageAssignAsnButtonLabel),
|
||||||
|
onPressed: widget.allowEdit
|
||||||
|
? () => BlocProvider.of<DocumentsCubit>(context)
|
||||||
|
.assignAsn(document)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_separator(),
|
_separator(),
|
||||||
@@ -191,7 +218,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
_separator(),
|
_separator(),
|
||||||
_DetailsItem.text(formatBytes(meta.originalSize, 2),
|
_DetailsItem.text(formatBytes(meta.originalSize, 2),
|
||||||
label: S.of(context).documentMetaDataOriginalFileSizeLabel, context: context),
|
label: S.of(context).documentMetaDataOriginalFileSizeLabel,
|
||||||
|
context: context),
|
||||||
_separator(),
|
_separator(),
|
||||||
_DetailsItem.text(
|
_DetailsItem.text(
|
||||||
meta.originalMimeType,
|
meta.originalMimeType,
|
||||||
@@ -239,6 +267,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
_separator(),
|
_separator(),
|
||||||
_DetailsItem(
|
_DetailsItem(
|
||||||
content: DocumentTypeWidget(
|
content: DocumentTypeWidget(
|
||||||
|
isClickable: widget.isLabelClickable,
|
||||||
documentTypeId: document.documentType,
|
documentTypeId: document.documentType,
|
||||||
afterSelected: () {
|
afterSelected: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -250,6 +279,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
_DetailsItem(
|
_DetailsItem(
|
||||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
content: CorrespondentWidget(
|
content: CorrespondentWidget(
|
||||||
|
isClickable: widget.isLabelClickable,
|
||||||
correspondentId: document.correspondent,
|
correspondentId: document.correspondent,
|
||||||
afterSelected: () {
|
afterSelected: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -260,6 +290,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
_DetailsItem(
|
_DetailsItem(
|
||||||
label: S.of(context).documentStoragePathPropertyLabel,
|
label: S.of(context).documentStoragePathPropertyLabel,
|
||||||
content: StoragePathWidget(
|
content: StoragePathWidget(
|
||||||
|
isClickable: widget.isLabelClickable,
|
||||||
pathId: document.storagePath,
|
pathId: document.storagePath,
|
||||||
afterSelected: () {
|
afterSelected: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -272,6 +303,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
content: Padding(
|
content: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: TagsWidget(
|
child: TagsWidget(
|
||||||
|
isClickable: widget.isLabelClickable,
|
||||||
tagIds: document.tags,
|
tagIds: document.tags,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -321,13 +353,15 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
|
|
||||||
Future<void> _onDownload(DocumentModel document) async {
|
Future<void> _onDownload(DocumentModel document) async {
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
showSnackBar(context, "This feature is currently only supported on Android!");
|
showSnackBar(
|
||||||
|
context, "This feature is currently only supported on Android!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() => _isDownloadPending = true);
|
setState(() => _isDownloadPending = true);
|
||||||
getIt<DocumentRepository>().download(document).then((bytes) async {
|
getIt<DocumentRepository>().download(document).then((bytes) async {
|
||||||
final Directory dir =
|
final Directory dir = (await getExternalStorageDirectories(
|
||||||
(await getExternalStorageDirectories(type: StorageDirectory.downloads))!.first;
|
type: StorageDirectory.downloads))!
|
||||||
|
.first;
|
||||||
String filePath = "${dir.path}/${document.originalFileName}";
|
String filePath = "${dir.path}/${document.originalFileName}";
|
||||||
//TODO: Add replacement mechanism here (ask user if file should be replaced if exists)
|
//TODO: Add replacement mechanism here (ask user if file should be replaced if exists)
|
||||||
await File(filePath).writeAsBytes(bytes);
|
await File(filePath).writeAsBytes(bytes);
|
||||||
@@ -340,7 +374,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
/// Downloads file to temporary directory, from which it can then be shared.
|
/// Downloads file to temporary directory, from which it can then be shared.
|
||||||
///
|
///
|
||||||
Future<void> _onShare(DocumentModel document) async {
|
Future<void> _onShare(DocumentModel document) async {
|
||||||
Uint8List documentBytes = await getIt<DocumentRepository>().download(document);
|
Uint8List documentBytes =
|
||||||
|
await getIt<DocumentRepository>().download(document);
|
||||||
final dir = await getTemporaryDirectory();
|
final dir = await getTemporaryDirectory();
|
||||||
final String path = "${dir.path}/${document.originalFileName}";
|
final String path = "${dir.path}/${document.originalFileName}";
|
||||||
await File(path).writeAsBytes(documentBytes);
|
await File(path).writeAsBytes(documentBytes);
|
||||||
@@ -359,14 +394,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
|
|
||||||
Future<void> _onDelete(DocumentModel document) async {
|
Future<void> _onDelete(DocumentModel document) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => DeleteDocumentConfirmationDialog(document: document)).then((delete) {
|
builder: (context) =>
|
||||||
|
DeleteDocumentConfirmationDialog(document: document))
|
||||||
|
.then((delete) {
|
||||||
if (delete ?? false) {
|
if (delete ?? false) {
|
||||||
BlocProvider.of<DocumentsCubit>(context).removeDocument(document).then((value) {
|
BlocProvider.of<DocumentsCubit>(context)
|
||||||
|
.removeDocument(document)
|
||||||
|
.then((value) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
showSnackBar(context, S.of(context).documentDeleteSuccessMessage);
|
showSnackBar(context, S.of(context).documentDeleteSuccessMessage);
|
||||||
}).onError<ErrorMessage>((error, _) {
|
|
||||||
showSnackBar(context, translateError(context, error.code));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -384,14 +421,17 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
if (bytes <= 0) return "0 B";
|
if (bytes <= 0) return "0 B";
|
||||||
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
var i = (log(bytes) / log(1024)).floor();
|
var i = (log(bytes) / log(1024)).floor();
|
||||||
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i];
|
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) +
|
||||||
|
' ' +
|
||||||
|
suffixes[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailsItem extends StatelessWidget {
|
class _DetailsItem extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final Widget content;
|
final Widget content;
|
||||||
const _DetailsItem({Key? key, required this.label, required this.content}) : super(key: key);
|
const _DetailsItem({Key? key, required this.label, required this.content})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -402,7 +442,10 @@ class _DetailsItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headline5
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
content,
|
content,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
});
|
});
|
||||||
await getIt<DocumentsCubit>().updateDocument(updatedDocument);
|
await getIt<DocumentsCubit>().updateDocument(updatedDocument);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
showSnackBar(context, "Document successfully updated."); //TODO: INTL
|
showSnackBar(
|
||||||
|
context, "Document successfully updated."); //TODO: INTL
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
@@ -111,18 +112,21 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
return LabelFormField<DocumentType, DocumentTypeQuery>(
|
return LabelFormField<DocumentType, DocumentTypeQuery>(
|
||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (currentInput) => BlocProvider.value(
|
labelCreationWidgetBuilder: (currentInput) =>
|
||||||
|
BlocProvider.value(
|
||||||
value: BlocProvider.of<DocumentTypeCubit>(context),
|
value: BlocProvider.of<DocumentTypeCubit>(context),
|
||||||
child: AddDocumentTypePage(
|
child: AddDocumentTypePage(
|
||||||
initialName: currentInput,
|
initialName: currentInput,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: S.of(context).documentDocumentTypePropertyLabel,
|
label: S.of(context).documentDocumentTypePropertyLabel,
|
||||||
initialValue: DocumentTypeQuery.fromId(widget.document.documentType),
|
initialValue:
|
||||||
|
DocumentTypeQuery.fromId(widget.document.documentType),
|
||||||
state: state,
|
state: state,
|
||||||
name: fkDocumentType,
|
name: fkDocumentType,
|
||||||
queryParameterIdBuilder: DocumentTypeQuery.fromId,
|
queryParameterIdBuilder: DocumentTypeQuery.fromId,
|
||||||
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
|
queryParameterNotAssignedBuilder:
|
||||||
|
DocumentTypeQuery.notAssigned,
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -132,16 +136,19 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
return LabelFormField<Correspondent, CorrespondentQuery>(
|
return LabelFormField<Correspondent, CorrespondentQuery>(
|
||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
|
labelCreationWidgetBuilder: (initialValue) =>
|
||||||
|
BlocProvider.value(
|
||||||
value: BlocProvider.of<CorrespondentCubit>(context),
|
value: BlocProvider.of<CorrespondentCubit>(context),
|
||||||
child: AddCorrespondentPage(initalValue: initialValue),
|
child: AddCorrespondentPage(initalValue: initialValue),
|
||||||
),
|
),
|
||||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
state: state,
|
state: state,
|
||||||
initialValue: CorrespondentQuery.fromId(widget.document.correspondent),
|
initialValue:
|
||||||
|
CorrespondentQuery.fromId(widget.document.correspondent),
|
||||||
name: fkCorrespondent,
|
name: fkCorrespondent,
|
||||||
queryParameterIdBuilder: CorrespondentQuery.fromId,
|
queryParameterIdBuilder: CorrespondentQuery.fromId,
|
||||||
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
|
queryParameterNotAssignedBuilder:
|
||||||
|
CorrespondentQuery.notAssigned,
|
||||||
prefixIcon: const Icon(Icons.person_outlined),
|
prefixIcon: const Icon(Icons.person_outlined),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -151,16 +158,19 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
return LabelFormField<StoragePath, StoragePathQuery>(
|
return LabelFormField<StoragePath, StoragePathQuery>(
|
||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
|
labelCreationWidgetBuilder: (initialValue) =>
|
||||||
|
BlocProvider.value(
|
||||||
value: BlocProvider.of<StoragePathCubit>(context),
|
value: BlocProvider.of<StoragePathCubit>(context),
|
||||||
child: AddStoragePathPage(initalValue: initialValue),
|
child: AddStoragePathPage(initalValue: initialValue),
|
||||||
),
|
),
|
||||||
label: S.of(context).documentStoragePathPropertyLabel,
|
label: S.of(context).documentStoragePathPropertyLabel,
|
||||||
state: state,
|
state: state,
|
||||||
initialValue: StoragePathQuery.fromId(widget.document.storagePath),
|
initialValue:
|
||||||
|
StoragePathQuery.fromId(widget.document.storagePath),
|
||||||
name: fkStoragePath,
|
name: fkStoragePath,
|
||||||
queryParameterIdBuilder: StoragePathQuery.fromId,
|
queryParameterIdBuilder: StoragePathQuery.fromId,
|
||||||
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
|
queryParameterNotAssignedBuilder:
|
||||||
|
StoragePathQuery.notAssigned,
|
||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
prefixIcon: const Icon(Icons.folder_outlined),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,12 +46,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
if (!documentsCubit.state.isLoaded) {
|
if (!documentsCubit.state.isLoaded) {
|
||||||
documentsCubit.loadDocuments().onError<ErrorMessage>(
|
documentsCubit.loadDocuments();
|
||||||
(error, stackTrace) => showSnackBar(
|
|
||||||
context,
|
|
||||||
translateError(context, error.code),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_pagingController.addPageRequestListener(_loadNewPage);
|
_pagingController.addPageRequestListener(_loadNewPage);
|
||||||
}
|
}
|
||||||
@@ -64,8 +59,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
|
|
||||||
Future<void> _loadNewPage(int pageKey) async {
|
Future<void> _loadNewPage(int pageKey) async {
|
||||||
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
final pageCount =
|
final pageCount = documentsCubit.state
|
||||||
documentsCubit.state.inferPageCount(pageSize: documentsCubit.state.filter.pageSize);
|
.inferPageCount(pageSize: documentsCubit.state.filter.pageSize);
|
||||||
if (pageCount <= pageKey + 1) {
|
if (pageCount <= pageKey + 1) {
|
||||||
_pagingController.nextPageKey = null;
|
_pagingController.nextPageKey = null;
|
||||||
}
|
}
|
||||||
@@ -78,11 +73,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
|
|
||||||
Future<void> _onRefresh() {
|
Future<void> _onRefresh() {
|
||||||
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
return documentsCubit
|
return documentsCubit.updateFilter(
|
||||||
.updateFilter(filter: documentsCubit.state.filter.copyWith(page: 1))
|
filter: documentsCubit.state.filter.copyWith(page: 1));
|
||||||
.onError<ErrorMessage>((error, _) {
|
|
||||||
showSnackBar(context, translateError(context, error.code));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -103,7 +95,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
},
|
},
|
||||||
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous != ConnectivityState.connected && current == ConnectivityState.connected,
|
previous != ConnectivityState.connected &&
|
||||||
|
current == ConnectivityState.connected,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
BlocProvider.of<DocumentsCubit>(context).loadDocuments();
|
BlocProvider.of<DocumentsCubit>(context).loadDocuments();
|
||||||
},
|
},
|
||||||
@@ -114,7 +107,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
child: const InfoDrawer(),
|
child: const InfoDrawer(),
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: connectivityState == ConnectivityState.connected ? null : const OfflineBanner(),
|
|
||||||
body: SlidingUpPanel(
|
body: SlidingUpPanel(
|
||||||
backdropEnabled: true,
|
backdropEnabled: true,
|
||||||
parallaxEnabled: true,
|
parallaxEnabled: true,
|
||||||
@@ -122,7 +114,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
controller: _panelController,
|
controller: _panelController,
|
||||||
defaultPanelState: PanelState.CLOSED,
|
defaultPanelState: PanelState.CLOSED,
|
||||||
minHeight: 48,
|
minHeight: 48,
|
||||||
maxHeight: MediaQuery.of(context).size.height - kBottomNavigationBarHeight,
|
maxHeight: MediaQuery.of(context).size.height -
|
||||||
|
kBottomNavigationBarHeight,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(16),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(16),
|
topRight: Radius.circular(16),
|
||||||
@@ -157,7 +150,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
state: state,
|
state: state,
|
||||||
onSelected: _onSelected,
|
onSelected: _onSelected,
|
||||||
pagingController: _pagingController,
|
pagingController: _pagingController,
|
||||||
hasInternetConnection: connectivityState == ConnectivityState.connected,
|
hasInternetConnection:
|
||||||
|
connectivityState == ConnectivityState.connected,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case ViewType.grid:
|
case ViewType.grid:
|
||||||
@@ -166,7 +160,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
state: state,
|
state: state,
|
||||||
onSelected: _onSelected,
|
onSelected: _onSelected,
|
||||||
pagingController: _pagingController,
|
pagingController: _pagingController,
|
||||||
hasInternetConnection: connectivityState == ConnectivityState.connected);
|
hasInternetConnection:
|
||||||
|
connectivityState == ConnectivityState.connected);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,9 +186,12 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
const SortDocumentsButton(),
|
const SortDocumentsButton(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_viewType == ViewType.grid ? Icons.list : Icons.grid_view,
|
_viewType == ViewType.grid
|
||||||
|
? Icons.list
|
||||||
|
: Icons.grid_view,
|
||||||
),
|
),
|
||||||
onPressed: () => setState(() => _viewType = _viewType.toggle()),
|
onPressed: () =>
|
||||||
|
setState(() => _viewType = _viewType.toggle()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
|
Text(
|
||||||
|
S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -36,7 +37,8 @@ class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
foregroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
foregroundColor:
|
||||||
|
MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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/logic/error_code_localization_mapper.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
@@ -7,6 +9,7 @@ import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
|||||||
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
import 'package:paperless_mobile/util.dart';
|
||||||
|
|
||||||
class DocumentsEmptyState extends StatelessWidget {
|
class DocumentsEmptyState extends StatelessWidget {
|
||||||
final DocumentsState state;
|
final DocumentsState state;
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CorrespondentWidget(correspondentId: document.correspondent),
|
CorrespondentWidget(
|
||||||
DocumentTypeWidget(documentTypeId: document.documentType),
|
correspondentId: document.correspondent),
|
||||||
|
DocumentTypeWidget(
|
||||||
|
documentTypeId: document.documentType),
|
||||||
Text(
|
Text(
|
||||||
document.title,
|
document.title,
|
||||||
maxLines: document.tags.isEmpty ? 3 : 2,
|
maxLines: document.tags.isEmpty ? 3 : 2,
|
||||||
@@ -64,7 +66,8 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
tagIds: document.tags,
|
tagIds: document.tags,
|
||||||
isMultiLine: false,
|
isMultiLine: false,
|
||||||
),
|
),
|
||||||
Text(DateFormat.yMMMd(Intl.getCurrentLocale()).format(document.created)),
|
Text(DateFormat.yMMMd(Intl.getCurrentLocale())
|
||||||
|
.format(document.created)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import 'package:paperless_mobile/features/documents/view/widgets/list/document_l
|
|||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
class DocumentListView extends StatelessWidget {
|
class DocumentListView extends StatelessWidget {
|
||||||
final void Function(DocumentModel model) onTap;
|
final void Function(DocumentModel) onTap;
|
||||||
final void Function(DocumentModel) onSelected;
|
final void Function(DocumentModel) onSelected;
|
||||||
|
|
||||||
final PagingController<int, DocumentModel> pagingController;
|
final PagingController<int, DocumentModel> pagingController;
|
||||||
final DocumentsState state;
|
final DocumentsState state;
|
||||||
final bool hasInternetConnection;
|
final bool hasInternetConnection;
|
||||||
|
final bool isLabelClickable;
|
||||||
const DocumentListView({
|
const DocumentListView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
@@ -20,24 +21,28 @@ class DocumentListView extends StatelessWidget {
|
|||||||
required this.state,
|
required this.state,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
required this.hasInternetConnection,
|
required this.hasInternetConnection,
|
||||||
|
this.isLabelClickable = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PagedSliverList<int, DocumentModel>(
|
return PagedSliverList<int, DocumentModel>(
|
||||||
pagingController: pagingController,
|
pagingController: pagingController,
|
||||||
builderDelegate: PagedChildBuilderDelegate(
|
builderDelegate: PagedChildBuilderDelegate(
|
||||||
animateTransitions: true,
|
animateTransitions: true,
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, document, index) {
|
||||||
return DocumentListItem(
|
return DocumentListItem(
|
||||||
document: item,
|
isLabelClickable: isLabelClickable,
|
||||||
|
document: document,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
isSelected: state.selection.contains(item),
|
isSelected: state.selection.contains(document),
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
isAtLeastOneSelected: state.selection.isNotEmpty,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
noItemsFoundIndicatorBuilder: (context) =>
|
noItemsFoundIndicatorBuilder: (context) => hasInternetConnection
|
||||||
hasInternetConnection ? const DocumentsListLoadingWidget() : const OfflineWidget(),
|
? const DocumentsListLoadingWidget()
|
||||||
|
: const OfflineWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import 'package:paperless_mobile/features/labels/correspondent/view/widgets/corr
|
|||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
|
|
||||||
class DocumentListItem extends StatelessWidget {
|
class DocumentListItem extends StatelessWidget {
|
||||||
static const a4AspectRatio = 1 / 1.4142;
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
final bool isSelected;
|
|
||||||
final void Function(DocumentModel) onTap;
|
final void Function(DocumentModel) onTap;
|
||||||
final void Function(DocumentModel)? onSelected;
|
final void Function(DocumentModel)? onSelected;
|
||||||
|
final bool isSelected;
|
||||||
final bool isAtLeastOneSelected;
|
final bool isAtLeastOneSelected;
|
||||||
|
final bool isLabelClickable;
|
||||||
|
|
||||||
const DocumentListItem({
|
const DocumentListItem({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -19,6 +20,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
this.onSelected,
|
this.onSelected,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
required this.isAtLeastOneSelected,
|
required this.isAtLeastOneSelected,
|
||||||
|
this.isLabelClickable = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,6 +41,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: isAtLeastOneSelected,
|
absorbing: isAtLeastOneSelected,
|
||||||
child: CorrespondentWidget(
|
child: CorrespondentWidget(
|
||||||
|
isClickable: isLabelClickable,
|
||||||
correspondentId: document.correspondent,
|
correspondentId: document.correspondent,
|
||||||
afterSelected: () {},
|
afterSelected: () {},
|
||||||
),
|
),
|
||||||
@@ -57,6 +60,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
absorbing: isAtLeastOneSelected,
|
absorbing: isAtLeastOneSelected,
|
||||||
child: TagsWidget(
|
child: TagsWidget(
|
||||||
|
isClickable: isLabelClickable,
|
||||||
tagIds: document.tags,
|
tagIds: document.tags,
|
||||||
isMultiLine: false,
|
isMultiLine: false,
|
||||||
),
|
),
|
||||||
@@ -64,7 +68,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isThreeLine: document.tags.isNotEmpty,
|
isThreeLine: document.tags.isNotEmpty,
|
||||||
leading: AspectRatio(
|
leading: AspectRatio(
|
||||||
aspectRatio: a4AspectRatio,
|
aspectRatio: _a4AspectRatio,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: DocumentPreview(
|
child: DocumentPreview(
|
||||||
id: document.id,
|
id: document.id,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
@@ -24,6 +26,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie
|
|||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:paperless_mobile/util.dart';
|
||||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||||
|
|
||||||
enum DateRangeSelection { before, after }
|
enum DateRangeSelection { before, after }
|
||||||
@@ -108,7 +111,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: Text(S.of(context).documentsFilterPageResetFilterLabel),
|
label: Text(
|
||||||
|
S.of(context).documentsFilterPageResetFilterLabel),
|
||||||
onPressed: () => _resetFilter(context),
|
onPressed: () => _resetFilter(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -126,7 +130,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _onApplyFilter,
|
onPressed: _onApplyFilter,
|
||||||
child: Text(S.of(context).documentsFilterPageApplyFilterLabel),
|
child: Text(
|
||||||
|
S.of(context).documentsFilterPageApplyFilterLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padded(),
|
).padded(),
|
||||||
@@ -225,15 +230,17 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQueryFormField(DocumentsState state) {
|
Widget _buildQueryFormField(DocumentsState state) {
|
||||||
final queryType = _formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ??
|
final queryType =
|
||||||
QueryType.titleAndContent;
|
_formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ??
|
||||||
|
QueryType.titleAndContent;
|
||||||
late String label;
|
late String label;
|
||||||
switch (queryType) {
|
switch (queryType) {
|
||||||
case QueryType.title:
|
case QueryType.title:
|
||||||
label = S.of(context).documentsFilterPageQueryOptionsTitleLabel;
|
label = S.of(context).documentsFilterPageQueryOptionsTitleLabel;
|
||||||
break;
|
break;
|
||||||
case QueryType.titleAndContent:
|
case QueryType.titleAndContent:
|
||||||
label = S.of(context).documentsFilterPageQueryOptionsTitleAndContentLabel;
|
label =
|
||||||
|
S.of(context).documentsFilterPageQueryOptionsTitleAndContentLabel;
|
||||||
break;
|
break;
|
||||||
case QueryType.extended:
|
case QueryType.extended:
|
||||||
label = S.of(context).documentsFilterPageQueryOptionsExtendedLabel;
|
label = S.of(context).documentsFilterPageQueryOptionsExtendedLabel;
|
||||||
@@ -255,7 +262,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
).padded();
|
).padded();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDateRangePickerHelper(DocumentsState state, String formFieldKey) {
|
Widget _buildDateRangePickerHelper(
|
||||||
|
DocumentsState state, String formFieldKey) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -279,10 +287,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final firstDayOfLastMonth = DateUtils.addMonthsToMonthDate(now, -1);
|
final firstDayOfLastMonth =
|
||||||
|
DateUtils.addMonthsToMonthDate(now, -1);
|
||||||
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
||||||
DateTimeRange(
|
DateTimeRange(
|
||||||
start: DateTime(firstDayOfLastMonth.year, firstDayOfLastMonth.month, now.day),
|
start: DateTime(firstDayOfLastMonth.year,
|
||||||
|
firstDayOfLastMonth.month, now.day),
|
||||||
end: DateTime.now(),
|
end: DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -294,7 +304,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final firstDayOfLastMonth = DateUtils.addMonthsToMonthDate(now, -3);
|
final firstDayOfLastMonth =
|
||||||
|
DateUtils.addMonthsToMonthDate(now, -3);
|
||||||
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
||||||
DateTimeRange(
|
DateTimeRange(
|
||||||
start: DateTime(
|
start: DateTime(
|
||||||
@@ -313,7 +324,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final firstDayOfLastMonth = DateUtils.addMonthsToMonthDate(now, -12);
|
final firstDayOfLastMonth =
|
||||||
|
DateUtils.addMonthsToMonthDate(now, -12);
|
||||||
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
_formKey.currentState?.fields[formFieldKey]?.didChange(
|
||||||
DateTimeRange(
|
DateTimeRange(
|
||||||
start: DateTime(
|
start: DateTime(
|
||||||
@@ -345,7 +357,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
||||||
iconTheme: IconThemeData(color: Theme.of(context).primaryColor),
|
iconTheme:
|
||||||
|
IconThemeData(color: Theme.of(context).primaryColor),
|
||||||
),
|
),
|
||||||
colorScheme: Theme.of(context).colorScheme.copyWith(
|
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||||
onPrimary: Theme.of(context).primaryColor,
|
onPrimary: Theme.of(context).primaryColor,
|
||||||
@@ -355,8 +368,10 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
|
format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
|
||||||
fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel,
|
fieldStartLabelText:
|
||||||
fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel,
|
S.of(context).documentsFilterPageDateRangeFieldStartLabel,
|
||||||
|
fieldEndLabelText:
|
||||||
|
S.of(context).documentsFilterPageDateRangeFieldEndLabel,
|
||||||
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
|
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
|
||||||
lastDate: DateTime.now(),
|
lastDate: DateTime.now(),
|
||||||
name: fkCreatedAt,
|
name: fkCreatedAt,
|
||||||
@@ -365,7 +380,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
onPressed: () => _formKey.currentState?.fields[fkCreatedAt]?.didChange(null),
|
onPressed: () =>
|
||||||
|
_formKey.currentState?.fields[fkCreatedAt]?.didChange(null),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -388,7 +404,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
||||||
iconTheme: IconThemeData(color: Theme.of(context).primaryColor),
|
iconTheme:
|
||||||
|
IconThemeData(color: Theme.of(context).primaryColor),
|
||||||
),
|
),
|
||||||
colorScheme: Theme.of(context).colorScheme.copyWith(
|
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||||
onPrimary: Theme.of(context).primaryColor,
|
onPrimary: Theme.of(context).primaryColor,
|
||||||
@@ -398,8 +415,10 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
|
format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
|
||||||
fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel,
|
fieldStartLabelText:
|
||||||
fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel,
|
S.of(context).documentsFilterPageDateRangeFieldStartLabel,
|
||||||
|
fieldEndLabelText:
|
||||||
|
S.of(context).documentsFilterPageDateRangeFieldEndLabel,
|
||||||
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
|
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
|
||||||
lastDate: DateTime.now(),
|
lastDate: DateTime.now(),
|
||||||
name: fkAddedAt,
|
name: fkAddedAt,
|
||||||
@@ -408,7 +427,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
labelText: S.of(context).documentAddedPropertyLabel,
|
labelText: S.of(context).documentAddedPropertyLabel,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
onPressed: () => _formKey.currentState?.fields[fkAddedAt]?.didChange(null),
|
onPressed: () =>
|
||||||
|
_formKey.currentState?.fields[fkAddedAt]?.didChange(null),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -444,16 +464,16 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
separatorBuilder: (context, index) => const SizedBox(
|
separatorBuilder: (context, index) => const SizedBox(
|
||||||
width: 8.0,
|
width: 8.0,
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) => _buildActionChip(
|
||||||
_buildActionChip(_sortFields[index], state.filter.sortField, context),
|
_sortFields[index], state.filter.sortField, context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padded();
|
).padded();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionChip(
|
Widget _buildActionChip(SortField sortField,
|
||||||
SortField sortField, SortField? currentlySelectedOrder, BuildContext context) {
|
SortField? currentlySelectedOrder, BuildContext context) {
|
||||||
String text;
|
String text;
|
||||||
switch (sortField) {
|
switch (sortField) {
|
||||||
case SortField.archiveSerialNumber:
|
case SortField.archiveSerialNumber:
|
||||||
@@ -488,8 +508,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
onPressed: () =>
|
onPressed: () => docBloc.updateFilter(
|
||||||
docBloc.updateFilter(filter: docBloc.state.filter.copyWith(sortField: sortField)),
|
filter: docBloc.state.filter.copyWith(sortField: sortField)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +530,9 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
|
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
|
||||||
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
|
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
|
||||||
);
|
);
|
||||||
BlocProvider.of<DocumentsCubit>(context).updateFilter(filter: newFilter).then((value) {
|
BlocProvider.of<DocumentsCubit>(context)
|
||||||
|
.updateFilter(filter: newFilter)
|
||||||
|
.then((value) {
|
||||||
BlocProvider.of<SavedViewCubit>(context).resetSelection();
|
BlocProvider.of<SavedViewCubit>(context).resetSelection();
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
widget.panelController.close();
|
widget.panelController.close();
|
||||||
|
|||||||
@@ -20,19 +20,23 @@ class QueryTypeFormField extends StatelessWidget {
|
|||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(S.of(context).documentsFilterPageQueryOptionsTitleAndContentLabel),
|
title: Text(S
|
||||||
|
.of(context)
|
||||||
|
.documentsFilterPageQueryOptionsTitleAndContentLabel),
|
||||||
),
|
),
|
||||||
value: QueryType.titleAndContent,
|
value: QueryType.titleAndContent,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(S.of(context).documentsFilterPageQueryOptionsTitleLabel),
|
title:
|
||||||
|
Text(S.of(context).documentsFilterPageQueryOptionsTitleLabel),
|
||||||
),
|
),
|
||||||
value: QueryType.title,
|
value: QueryType.title,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(S.of(context).documentsFilterPageQueryOptionsExtendedLabel),
|
title: Text(
|
||||||
|
S.of(context).documentsFilterPageQueryOptionsExtendedLabel),
|
||||||
),
|
),
|
||||||
value: QueryType.extended,
|
value: QueryType.extended,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
|||||||
SavedView.fromDocumentFilter(
|
SavedView.fromDocumentFilter(
|
||||||
widget.currentFilter,
|
widget.currentFilter,
|
||||||
name: _formKey.currentState?.value[fkName] as String,
|
name: _formKey.currentState?.value[fkName] as String,
|
||||||
showOnDashboard: _formKey.currentState?.value[fkShowOnDashboard] as bool,
|
showOnDashboard:
|
||||||
|
_formKey.currentState?.value[fkShowOnDashboard] as bool,
|
||||||
showInSidebar: _formKey.currentState?.value[fkShowInSidebar] as bool,
|
showInSidebar: _formKey.currentState?.value[fkShowInSidebar] as bool,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
|||||||
class BulkDeleteConfirmationDialog extends StatelessWidget {
|
class BulkDeleteConfirmationDialog extends StatelessWidget {
|
||||||
static const _bulletPoint = "\u2022";
|
static const _bulletPoint = "\u2022";
|
||||||
final DocumentsState state;
|
final DocumentsState state;
|
||||||
const BulkDeleteConfirmationDialog({Key? key, required this.state}) : super(key: key);
|
const BulkDeleteConfirmationDialog({Key? key, required this.state})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -19,8 +20,12 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
//TODO: use plurals, didn't use because of crash... investigate later.
|
//TODO: use plurals, didn't use because of crash... investigate later.
|
||||||
state.selection.length == 1
|
state.selection.length == 1
|
||||||
? S.of(context).documentsPageSelectionBulkDeleteDialogWarningTextOne
|
? S
|
||||||
: S.of(context).documentsPageSelectionBulkDeleteDialogWarningTextMany,
|
.of(context)
|
||||||
|
.documentsPageSelectionBulkDeleteDialogWarningTextOne
|
||||||
|
: S
|
||||||
|
.of(context)
|
||||||
|
.documentsPageSelectionBulkDeleteDialogWarningTextMany,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
@@ -31,7 +36,8 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
|
Text(
|
||||||
|
S.of(context).documentsPageSelectionBulkDeleteDialogContinueText),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -41,7 +47,8 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
foregroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
foregroundColor:
|
||||||
|
MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
|
|||||||
@@ -36,10 +36,11 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
|
|||||||
expandedHeight: kToolbarHeight,
|
expandedHeight: kToolbarHeight,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => BlocProvider.of<DocumentsCubit>(context).resetSelection(),
|
onPressed: () =>
|
||||||
|
BlocProvider.of<DocumentsCubit>(context).resetSelection(),
|
||||||
),
|
),
|
||||||
title:
|
title: Text(
|
||||||
Text('${documentsState.selection.length} ${S.of(context).documentsSelectedText}'),
|
'${documentsState.selection.length} ${S.of(context).documentsSelectedText}'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
@@ -79,9 +80,8 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
|
|||||||
if (shouldDelete ?? false) {
|
if (shouldDelete ?? false) {
|
||||||
BlocProvider.of<DocumentsCubit>(context)
|
BlocProvider.of<DocumentsCubit>(context)
|
||||||
.bulkRemoveDocuments(documentsState.selection)
|
.bulkRemoveDocuments(documentsState.selection)
|
||||||
.then((_) => showSnackBar(context, S.of(context).documentsPageBulkDeleteSuccessfulText))
|
.then((_) => showSnackBar(
|
||||||
.onError<ErrorMessage>(
|
context, S.of(context).documentsPageBulkDeleteSuccessfulText));
|
||||||
(error, _) => showSnackBar(context, translateError(context, error.code)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ class SavedViewSelectionWidget extends StatelessWidget {
|
|||||||
child: FilterChip(
|
child: FilterChip(
|
||||||
label: Text(state.value.values.toList()[index].name),
|
label: Text(state.value.values.toList()[index].name),
|
||||||
selected: view.id == state.selectedSavedViewId,
|
selected: view.id == state.selectedSavedViewId,
|
||||||
onSelected: (isSelected) => _onSelected(isSelected, context, view),
|
onSelected: (isSelected) =>
|
||||||
|
_onSelected(isSelected, context, view),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -76,21 +77,19 @@ class SavedViewSelectionWidget extends StatelessWidget {
|
|||||||
void _onCreatePressed(BuildContext context) async {
|
void _onCreatePressed(BuildContext context) async {
|
||||||
final newView = await Navigator.of(context).push<SavedView?>(
|
final newView = await Navigator.of(context).push<SavedView?>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddSavedViewPage(currentFilter: getIt<DocumentsCubit>().state.filter),
|
builder: (context) => AddSavedViewPage(
|
||||||
|
currentFilter: getIt<DocumentsCubit>().state.filter),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (newView != null) {
|
if (newView != null) {
|
||||||
try {
|
BlocProvider.of<SavedViewCubit>(context).add(newView);
|
||||||
BlocProvider.of<SavedViewCubit>(context).add(newView);
|
|
||||||
} on ErrorMessage catch (error) {
|
|
||||||
showError(context, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelected(bool isSelected, BuildContext context, SavedView view) {
|
void _onSelected(bool isSelected, BuildContext context, SavedView view) {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
BlocProvider.of<DocumentsCubit>(context).updateFilter(filter: view.toDocumentFilter());
|
BlocProvider.of<DocumentsCubit>(context)
|
||||||
|
.updateFilter(filter: view.toDocumentFilter());
|
||||||
BlocProvider.of<SavedViewCubit>(context).selectView(view);
|
BlocProvider.of<SavedViewCubit>(context).selectView(view);
|
||||||
} else {
|
} else {
|
||||||
BlocProvider.of<DocumentsCubit>(context).updateFilter();
|
BlocProvider.of<DocumentsCubit>(context).updateFilter();
|
||||||
@@ -106,11 +105,7 @@ class SavedViewSelectionWidget extends StatelessWidget {
|
|||||||
) ??
|
) ??
|
||||||
false;
|
false;
|
||||||
if (delete) {
|
if (delete) {
|
||||||
try {
|
BlocProvider.of<SavedViewCubit>(context).remove(view);
|
||||||
BlocProvider.of<SavedViewCubit>(context).remove(view);
|
|
||||||
} on ErrorMessage catch (error) {
|
|
||||||
showError(context, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_order.dart';
|
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_order.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:paperless_mobile/util.dart';
|
||||||
|
|
||||||
class SortDocumentsButton extends StatefulWidget {
|
class SortDocumentsButton extends StatefulWidget {
|
||||||
const SortDocumentsButton({
|
const SortDocumentsButton({
|
||||||
@@ -30,16 +33,20 @@ class _SortDocumentsButtonState extends State<SortDocumentsButton> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final bool isAscending = state.filter.sortOrder == SortOrder.ascending;
|
final bool isAscending =
|
||||||
|
state.filter.sortOrder == SortOrder.ascending;
|
||||||
child = IconButton(
|
child = IconButton(
|
||||||
icon: FaIcon(
|
icon: FaIcon(
|
||||||
isAscending ? FontAwesomeIcons.arrowDownAZ : FontAwesomeIcons.arrowUpZA,
|
isAscending
|
||||||
|
? FontAwesomeIcons.arrowDownAZ
|
||||||
|
: FontAwesomeIcons.arrowUpZA,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
BlocProvider.of<DocumentsCubit>(context)
|
BlocProvider.of<DocumentsCubit>(context)
|
||||||
.updateFilter(
|
.updateFilter(
|
||||||
filter: state.filter.copyWith(sortOrder: state.filter.sortOrder.toggle()))
|
filter: state.filter
|
||||||
|
.copyWith(sortOrder: state.filter.sortOrder.toggle()))
|
||||||
.whenComplete(() => setState(() => _isLoading = false));
|
.whenComplete(() => setState(() => _isLoading = false));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
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/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
|
||||||
@@ -35,33 +38,46 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<ConnectivityCubit, ConnectivityState>(
|
return BlocListener<GlobalErrorCubit, GlobalErrorState>(
|
||||||
//Only re-initialize data if the connectivity changed from not connected to connected
|
|
||||||
listenWhen: (previous, current) =>
|
|
||||||
previous != ConnectivityState.connected && current == ConnectivityState.connected,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
initializeLabelData(context);
|
if (state.hasError) {
|
||||||
|
showSnackBar(context, translateError(context, state.error!.code));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||||
key: rootScaffoldKey,
|
//Only re-initialize data if the connectivity changed from not connected to connected
|
||||||
bottomNavigationBar: BottomNavBar(
|
listenWhen: (previous, current) =>
|
||||||
selectedIndex: _currentIndex,
|
current == ConnectivityState.connected,
|
||||||
onNavigationChanged: (index) => setState(() => _currentIndex = index),
|
listener: (context, state) {
|
||||||
),
|
initializeLabelData(context);
|
||||||
drawer: const InfoDrawer(),
|
},
|
||||||
body: [
|
builder: (context, connectivityState) {
|
||||||
MultiBlocProvider(
|
return Scaffold(
|
||||||
providers: [
|
appBar: connectivityState == ConnectivityState.connected
|
||||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
? null
|
||||||
],
|
: const OfflineBanner(),
|
||||||
child: const DocumentsPage(),
|
key: rootScaffoldKey,
|
||||||
),
|
bottomNavigationBar: BottomNavBar(
|
||||||
BlocProvider.value(
|
selectedIndex: _currentIndex,
|
||||||
value: getIt<DocumentScannerCubit>(),
|
onNavigationChanged: (index) =>
|
||||||
child: const ScannerPage(),
|
setState(() => _currentIndex = index),
|
||||||
),
|
),
|
||||||
const LabelsPage(),
|
drawer: const InfoDrawer(),
|
||||||
][_currentIndex],
|
body: [
|
||||||
|
MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||||
|
],
|
||||||
|
child: const DocumentsPage(),
|
||||||
|
),
|
||||||
|
BlocProvider.value(
|
||||||
|
value: getIt<DocumentScannerCubit>(),
|
||||||
|
child: const ScannerPage(),
|
||||||
|
),
|
||||||
|
const LabelsPage(),
|
||||||
|
][_currentIndex],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ class BottomNavBar extends StatelessWidget {
|
|||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
final void Function(int) onNavigationChanged;
|
final void Function(int) onNavigationChanged;
|
||||||
|
|
||||||
const BottomNavBar({Key? key, required this.selectedIndex, required this.onNavigationChanged})
|
const BottomNavBar(
|
||||||
|
{Key? key,
|
||||||
|
required this.selectedIndex,
|
||||||
|
required this.onNavigationChanged})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
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/global_error_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||||
@@ -11,12 +14,12 @@ import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'
|
|||||||
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
|
import 'package:url_launcher/link.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
class InfoDrawer extends StatelessWidget {
|
class InfoDrawer extends StatelessWidget {
|
||||||
const InfoDrawer({Key? key}) : super(key: key);
|
const InfoDrawer({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
@@ -37,10 +40,9 @@ class InfoDrawer extends StatelessWidget {
|
|||||||
).padded(const EdgeInsets.only(right: 8.0)),
|
).padded(const EdgeInsets.only(right: 8.0)),
|
||||||
Text(
|
Text(
|
||||||
S.of(context).appTitleText,
|
S.of(context).appTitleText,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context).textTheme.headline5!.copyWith(
|
||||||
.textTheme
|
color:
|
||||||
.headline5!
|
Theme.of(context).colorScheme.onPrimaryContainer),
|
||||||
.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -49,9 +51,14 @@ class InfoDrawer extends StatelessWidget {
|
|||||||
child: BlocBuilder<AuthenticationCubit, AuthenticationState>(
|
child: BlocBuilder<AuthenticationCubit, AuthenticationState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Text(
|
return Text(
|
||||||
state.authentication?.serverUrl.replaceAll(RegExp(r'https?://'), "") ?? "",
|
state.authentication?.serverUrl
|
||||||
|
.replaceAll(RegExp(r'https?://'), "") ??
|
||||||
|
"",
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -81,17 +88,39 @@ class InfoDrawer extends StatelessWidget {
|
|||||||
leading: const Icon(Icons.bug_report),
|
leading: const Icon(Icons.bug_report),
|
||||||
title: Text(S.of(context).appDrawerReportBugLabel),
|
title: Text(S.of(context).appDrawerReportBugLabel),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString("https://github.com/astubenbord/paperless-mobile/issues/new");
|
launchUrlString(
|
||||||
|
"https://github.com/astubenbord/paperless-mobile/issues/new");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
AboutListTile(
|
AboutListTile(
|
||||||
icon: const Icon(Icons.info),
|
icon: const Icon(Icons.info),
|
||||||
applicationIcon: const ImageIcon(AssetImage("assets/logos/paperless_logo_green.png")),
|
applicationIcon: const ImageIcon(
|
||||||
|
AssetImage("assets/logos/paperless_logo_green.png")),
|
||||||
applicationName: "Paperless Mobile",
|
applicationName: "Paperless Mobile",
|
||||||
applicationVersion: kPackageInfo.version + "+" + kPackageInfo.buildNumber,
|
applicationVersion:
|
||||||
|
kPackageInfo.version + "+" + kPackageInfo.buildNumber,
|
||||||
aboutBoxChildren: [
|
aboutBoxChildren: [
|
||||||
Text('${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'),
|
Text(
|
||||||
|
'${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'),
|
||||||
|
Link(
|
||||||
|
uri: Uri.parse(
|
||||||
|
"https://github.com/astubenbord/paperless-mobile"),
|
||||||
|
builder: (context, followLink) => GestureDetector(
|
||||||
|
onTap: followLink,
|
||||||
|
child: Text(
|
||||||
|
"https://github.com/astubenbord/paperless-mobile",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.tertiary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Credits",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
_buildOnboardingImageCredits(),
|
||||||
],
|
],
|
||||||
child: Text(S.of(context).appDrawerAboutLabel),
|
child: Text(S.of(context).appDrawerAboutLabel),
|
||||||
),
|
),
|
||||||
@@ -107,6 +136,7 @@ class InfoDrawer extends StatelessWidget {
|
|||||||
getIt<DocumentTypeCubit>().reset();
|
getIt<DocumentTypeCubit>().reset();
|
||||||
getIt<TagCubit>().reset();
|
getIt<TagCubit>().reset();
|
||||||
getIt<DocumentScannerCubit>().reset();
|
getIt<DocumentScannerCubit>().reset();
|
||||||
|
getIt<GlobalErrorCubit>().reset();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -114,4 +144,24 @@ class InfoDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Link _buildOnboardingImageCredits() {
|
||||||
|
return Link(
|
||||||
|
uri: Uri.parse(
|
||||||
|
"https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author"),
|
||||||
|
builder: (context, followLink) => Wrap(
|
||||||
|
children: [
|
||||||
|
const Text("Onboarding images by "),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: followLink,
|
||||||
|
child: Text(
|
||||||
|
"pch.vector",
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(" on Freepik.")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:injectable/injectable.dart';
|
|||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class CorrespondentCubit extends LabelCubit<Correspondent> {
|
class CorrespondentCubit extends LabelCubit<Correspondent> {
|
||||||
CorrespondentCubit(super.metaDataService);
|
CorrespondentCubit(super.metaDataService, super.errorCubit);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
@@ -12,11 +12,14 @@ class CorrespondentCubit extends LabelCubit<Correspondent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Correspondent> save(Correspondent item) => labelRepository.saveCorrespondent(item);
|
Future<Correspondent> save(Correspondent item) =>
|
||||||
|
labelRepository.saveCorrespondent(item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Correspondent> update(Correspondent item) => labelRepository.updateCorrespondent(item);
|
Future<Correspondent> update(Correspondent item) =>
|
||||||
|
labelRepository.updateCorrespondent(item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> delete(Correspondent item) => labelRepository.deleteCorrespondent(item);
|
Future<int> delete(Correspondent item) =>
|
||||||
|
labelRepository.deleteCorrespondent(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
@@ -20,7 +20,8 @@ class Correspondent extends Label {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Correspondent.fromJson(JSON json)
|
Correspondent.fromJson(JSON json)
|
||||||
: lastCorrespondence = DateTime.tryParse(json[lastCorrespondenceKey] ?? ''),
|
: lastCorrespondence =
|
||||||
|
DateTime.tryParse(json[lastCorrespondenceKey] ?? ''),
|
||||||
super.fromJson(json);
|
super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -23,19 +23,16 @@ class EditCorrespondentPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDelete(Correspondent correspondent, BuildContext context) async {
|
Future<void> _onDelete(
|
||||||
try {
|
Correspondent correspondent, BuildContext context) async {
|
||||||
await BlocProvider.of<CorrespondentCubit>(context).remove(correspondent);
|
await BlocProvider.of<CorrespondentCubit>(context).remove(correspondent);
|
||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
if (cubit.state.filter.correspondent.id == correspondent.id) {
|
if (cubit.state.filter.correspondent.id == correspondent.id) {
|
||||||
cubit.updateFilter(
|
cubit.updateFilter(
|
||||||
filter: cubit.state.filter.copyWith(correspondent: const CorrespondentQuery.unset()),
|
filter: cubit.state.filter
|
||||||
);
|
.copyWith(correspondent: const CorrespondentQuery.unset()),
|
||||||
}
|
);
|
||||||
} on ErrorMessage catch (e) {
|
|
||||||
showSnackBar(context, translateError(context, e.code));
|
|
||||||
} finally {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
}
|
||||||
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,11 +46,13 @@ class CorrespondentWidget extends StatelessWidget {
|
|||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
if (cubit.state.filter.correspondent.id == correspondentId) {
|
if (cubit.state.filter.correspondent.id == correspondentId) {
|
||||||
cubit.updateCurrentFilter(
|
cubit.updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(correspondent: const CorrespondentQuery.unset()),
|
(filter) =>
|
||||||
|
filter.copyWith(correspondent: const CorrespondentQuery.unset()),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cubit.updateCurrentFilter(
|
cubit.updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(correspondent: CorrespondentQuery.fromId(correspondentId)),
|
(filter) => filter.copyWith(
|
||||||
|
correspondent: CorrespondentQuery.fromId(correspondentId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
afterSelected?.call();
|
afterSelected?.call();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:injectable/injectable.dart';
|
|||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class DocumentTypeCubit extends LabelCubit<DocumentType> {
|
class DocumentTypeCubit extends LabelCubit<DocumentType> {
|
||||||
DocumentTypeCubit(super.metaDataService);
|
DocumentTypeCubit(super.metaDataService, super.errorCubit);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
@@ -12,11 +12,14 @@ class DocumentTypeCubit extends LabelCubit<DocumentType> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DocumentType> save(DocumentType item) => labelRepository.saveDocumentType(item);
|
Future<DocumentType> save(DocumentType item) =>
|
||||||
|
labelRepository.saveDocumentType(item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DocumentType> update(DocumentType item) => labelRepository.updateDocumentType(item);
|
Future<DocumentType> update(DocumentType item) =>
|
||||||
|
labelRepository.updateDocumentType(item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> delete(DocumentType item) => labelRepository.deleteDocumentType(item);
|
Future<int> delete(DocumentType item) =>
|
||||||
|
labelRepository.deleteDocumentType(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class EditDocumentTypePage extends StatelessWidget {
|
|||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
if (cubit.state.filter.documentType.id == docType.id) {
|
if (cubit.state.filter.documentType.id == docType.id) {
|
||||||
cubit.updateFilter(
|
cubit.updateFilter(
|
||||||
filter: cubit.state.filter.copyWith(documentType: const DocumentTypeQuery.unset()),
|
filter: cubit.state.filter
|
||||||
|
.copyWith(documentType: const DocumentTypeQuery.unset()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on ErrorMessage catch (e) {
|
} on ErrorMessage catch (e) {
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import 'package:paperless_mobile/features/labels/document_type/model/document_ty
|
|||||||
class DocumentTypeWidget extends StatelessWidget {
|
class DocumentTypeWidget extends StatelessWidget {
|
||||||
final int? documentTypeId;
|
final int? documentTypeId;
|
||||||
final void Function()? afterSelected;
|
final void Function()? afterSelected;
|
||||||
final bool isSelectable;
|
final bool isClickable;
|
||||||
const DocumentTypeWidget({
|
const DocumentTypeWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.documentTypeId,
|
required this.documentTypeId,
|
||||||
this.afterSelected,
|
this.afterSelected,
|
||||||
this.isSelectable = true,
|
this.isClickable = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AbsorbPointer(
|
return AbsorbPointer(
|
||||||
absorbing: !isSelectable,
|
absorbing: !isClickable,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _addDocumentTypeToFilter(context),
|
onTap: () => _addDocumentTypeToFilter(context),
|
||||||
child: BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
|
child: BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
|
||||||
@@ -41,11 +41,13 @@ class DocumentTypeWidget extends StatelessWidget {
|
|||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
if (cubit.state.filter.documentType.id == documentTypeId) {
|
if (cubit.state.filter.documentType.id == documentTypeId) {
|
||||||
cubit.updateCurrentFilter(
|
cubit.updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(documentType: const DocumentTypeQuery.unset()),
|
(filter) =>
|
||||||
|
filter.copyWith(documentType: const DocumentTypeQuery.unset()),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cubit.updateCurrentFilter(
|
cubit.updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(documentType: DocumentTypeQuery.fromId(documentTypeId)),
|
(filter) => filter.copyWith(
|
||||||
|
documentType: DocumentTypeQuery.fromId(documentTypeId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
afterSelected?.call();
|
afterSelected?.call();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||||
|
|
||||||
@@ -37,7 +37,8 @@ abstract class Label with EquatableMixin implements Comparable {
|
|||||||
name = json[nameKey],
|
name = json[nameKey],
|
||||||
slug = json[slugKey],
|
slug = json[slugKey],
|
||||||
match = json[matchKey],
|
match = json[matchKey],
|
||||||
matchingAlgorithm = MatchingAlgorithm.fromInt(json[matchingAlgorithmKey]),
|
matchingAlgorithm =
|
||||||
|
MatchingAlgorithm.fromInt(json[matchingAlgorithmKey]),
|
||||||
isInsensitive = json[isInsensitiveKey],
|
isInsensitive = json[isInsensitiveKey],
|
||||||
documentCount = json[documentCountKey];
|
documentCount = json[documentCountKey];
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Tag?> getTag(int id) async {
|
Future<Tag?> getTag(int id) async {
|
||||||
return getSingleResult("/api/tags/$id/", Tag.fromJson, ErrorCode.tagLoadFailed);
|
return getSingleResult(
|
||||||
|
"/api/tags/$id/", Tag.fromJson, ErrorCode.tagLoadFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,7 +40,9 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
ErrorCode.tagLoadFailed,
|
ErrorCode.tagLoadFailed,
|
||||||
minRequiredApiVersion: 2,
|
minRequiredApiVersion: 2,
|
||||||
);
|
);
|
||||||
return results.where((element) => ids?.contains(element.id) ?? true).toList();
|
return results
|
||||||
|
.where((element) => ids?.contains(element.id) ?? true)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -78,9 +81,11 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
encoding: Encoding.getByName("utf-8"),
|
encoding: Encoding.getByName("utf-8"),
|
||||||
);
|
);
|
||||||
if (response.statusCode == 201) {
|
if (response.statusCode == 201) {
|
||||||
return Correspondent.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
return Correspondent.fromJson(
|
||||||
|
jsonDecode(utf8.decode(response.bodyBytes)));
|
||||||
}
|
}
|
||||||
throw ErrorMessage(ErrorCode.correspondentCreateFailed, httpStatusCode: response.statusCode);
|
throw ErrorMessage(ErrorCode.correspondentCreateFailed,
|
||||||
|
httpStatusCode: response.statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -127,7 +132,8 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
@override
|
@override
|
||||||
Future<int> deleteCorrespondent(Correspondent correspondent) async {
|
Future<int> deleteCorrespondent(Correspondent correspondent) async {
|
||||||
assert(correspondent.id != null);
|
assert(correspondent.id != null);
|
||||||
final response = await httpClient.delete(Uri.parse('/api/correspondents/${correspondent.id}/'));
|
final response = await httpClient
|
||||||
|
.delete(Uri.parse('/api/correspondents/${correspondent.id}/'));
|
||||||
if (response.statusCode == 204) {
|
if (response.statusCode == 204) {
|
||||||
return correspondent.id!;
|
return correspondent.id!;
|
||||||
}
|
}
|
||||||
@@ -137,7 +143,8 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
@override
|
@override
|
||||||
Future<int> deleteDocumentType(DocumentType documentType) async {
|
Future<int> deleteDocumentType(DocumentType documentType) async {
|
||||||
assert(documentType.id != null);
|
assert(documentType.id != null);
|
||||||
final response = await httpClient.delete(Uri.parse('/api/document_types/${documentType.id}/'));
|
final response = await httpClient
|
||||||
|
.delete(Uri.parse('/api/document_types/${documentType.id}/'));
|
||||||
if (response.statusCode == 204) {
|
if (response.statusCode == 204) {
|
||||||
return documentType.id!;
|
return documentType.id!;
|
||||||
}
|
}
|
||||||
@@ -164,7 +171,8 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
encoding: Encoding.getByName("utf-8"),
|
encoding: Encoding.getByName("utf-8"),
|
||||||
);
|
);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return Correspondent.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
return Correspondent.fromJson(
|
||||||
|
jsonDecode(utf8.decode(response.bodyBytes)));
|
||||||
}
|
}
|
||||||
throw const ErrorMessage(ErrorCode.unknown);
|
throw const ErrorMessage(ErrorCode.unknown);
|
||||||
}
|
}
|
||||||
@@ -205,7 +213,8 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
@override
|
@override
|
||||||
Future<int> deleteStoragePath(StoragePath path) async {
|
Future<int> deleteStoragePath(StoragePath path) async {
|
||||||
assert(path.id != null);
|
assert(path.id != null);
|
||||||
final response = await httpClient.delete(Uri.parse('/api/storage_paths/${path.id}/'));
|
final response =
|
||||||
|
await httpClient.delete(Uri.parse('/api/storage_paths/${path.id}/'));
|
||||||
if (response.statusCode == 204) {
|
if (response.statusCode == 204) {
|
||||||
return path.id!;
|
return path.id!;
|
||||||
}
|
}
|
||||||
@@ -214,8 +223,8 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<StoragePath?> getStoragePath(int id) {
|
Future<StoragePath?> getStoragePath(int id) {
|
||||||
return getSingleResult("/api/storage_paths/?page=1&page_size=100000", StoragePath.fromJson,
|
return getSingleResult("/api/storage_paths/?page=1&page_size=100000",
|
||||||
ErrorCode.storagePathLoadFailed);
|
StoragePath.fromJson, ErrorCode.storagePathLoadFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -237,7 +246,8 @@ class LabelRepositoryImpl implements LabelRepository {
|
|||||||
if (response.statusCode == 201) {
|
if (response.statusCode == 201) {
|
||||||
return StoragePath.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
return StoragePath.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
|
||||||
}
|
}
|
||||||
throw ErrorMessage(ErrorCode.storagePathCreateFailed, httpStatusCode: response.statusCode);
|
throw ErrorMessage(ErrorCode.storagePathCreateFailed,
|
||||||
|
httpStatusCode: response.statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class StoragePathCubit extends LabelCubit<StoragePath> {
|
class StoragePathCubit extends LabelCubit<StoragePath> {
|
||||||
StoragePathCubit(super.metaDataService);
|
StoragePathCubit(super.metaDataService, super.errorCubit);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
@@ -13,11 +12,14 @@ class StoragePathCubit extends LabelCubit<StoragePath> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<StoragePath> save(StoragePath item) => labelRepository.saveStoragePath(item);
|
Future<StoragePath> save(StoragePath item) =>
|
||||||
|
labelRepository.saveStoragePath(item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<StoragePath> update(StoragePath item) => labelRepository.updateStoragePath(item);
|
Future<StoragePath> update(StoragePath item) =>
|
||||||
|
labelRepository.updateStoragePath(item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> delete(StoragePath item) => labelRepository.deleteStoragePath(item);
|
Future<int> delete(StoragePath item) =>
|
||||||
|
labelRepository.deleteStoragePath(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
|
|||||||
@@ -32,17 +32,13 @@ class EditStoragePathPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDelete(StoragePath path, BuildContext context) async {
|
Future<void> _onDelete(StoragePath path, BuildContext context) async {
|
||||||
try {
|
await BlocProvider.of<StoragePathCubit>(context).remove(path);
|
||||||
await BlocProvider.of<StoragePathCubit>(context).remove(path);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
if (cubit.state.filter.storagePath.id == path.id) {
|
||||||
if (cubit.state.filter.storagePath.id == path.id) {
|
cubit.updateFilter(
|
||||||
cubit.updateFilter(
|
filter: cubit.state.filter
|
||||||
filter: cubit.state.filter.copyWith(storagePath: const StoragePathQuery.unset()));
|
.copyWith(storagePath: const StoragePathQuery.unset()));
|
||||||
}
|
|
||||||
} on ErrorMessage catch (e) {
|
|
||||||
showSnackBar(context, translateError(context, e.code));
|
|
||||||
} finally {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
}
|
||||||
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/src/widgets/container.dart';
|
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
class StoragePathAutofillFormBuilderField extends StatefulWidget {
|
class StoragePathAutofillFormBuilderField extends StatefulWidget {
|
||||||
final String name;
|
final String name;
|
||||||
@@ -20,10 +18,10 @@ class StoragePathAutofillFormBuilderField extends StatefulWidget {
|
|||||||
_StoragePathAutofillFormBuilderFieldState();
|
_StoragePathAutofillFormBuilderFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StoragePathAutofillFormBuilderFieldState extends State<StoragePathAutofillFormBuilderField> {
|
class _StoragePathAutofillFormBuilderFieldState
|
||||||
|
extends State<StoragePathAutofillFormBuilderField> {
|
||||||
late final TextEditingController _textEditingController;
|
late final TextEditingController _textEditingController;
|
||||||
|
|
||||||
late String _exampleOutput;
|
|
||||||
late bool _showClearIcon;
|
late bool _showClearIcon;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -35,7 +33,6 @@ class _StoragePathAutofillFormBuilderFieldState extends State<StoragePathAutofil
|
|||||||
_showClearIcon = _textEditingController.text.isNotEmpty;
|
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
_exampleOutput = _buildExampleOutput(widget.initialValue ?? '');
|
|
||||||
_showClearIcon = widget.initialValue?.isNotEmpty ?? false;
|
_showClearIcon = widget.initialValue?.isNotEmpty ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +67,8 @@ class _StoragePathAutofillFormBuilderFieldState extends State<StoragePathAutofil
|
|||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
InputChip(
|
InputChip(
|
||||||
label: Text(S.of(context).documentArchiveSerialNumberPropertyLongLabel),
|
label: Text(
|
||||||
|
S.of(context).documentArchiveSerialNumberPropertyLongLabel),
|
||||||
onPressed: () => _addParameterToInput("{asn}", field),
|
onPressed: () => _addParameterToInput("{asn}", field),
|
||||||
),
|
),
|
||||||
InputChip(
|
InputChip(
|
||||||
@@ -138,22 +136,8 @@ class _StoragePathAutofillFormBuilderFieldState extends State<StoragePathAutofil
|
|||||||
final text = (field.value ?? "") + param;
|
final text = (field.value ?? "") + param;
|
||||||
field.didChange(text);
|
field.didChange(text);
|
||||||
_textEditingController.text = text;
|
_textEditingController.text = text;
|
||||||
}
|
_textEditingController.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: _textEditingController.text.length));
|
||||||
String _buildExampleOutput(String input) {
|
|
||||||
return input
|
|
||||||
.replaceAll("{asn}", "1234")
|
|
||||||
.replaceAll("{correspondent}", "My Bank")
|
|
||||||
.replaceAll("{document_type}", "Invoice")
|
|
||||||
.replaceAll("{tag_list}", "TODO,University,Work")
|
|
||||||
.replaceAll("{created}", "2020-02-10")
|
|
||||||
.replaceAll("{created_year}", "2020")
|
|
||||||
.replaceAll("{created_month}", "02")
|
|
||||||
.replaceAll("{created_day}", "10")
|
|
||||||
.replaceAll("{added}", "2029-12-24")
|
|
||||||
.replaceAll("{added_year}", "2029")
|
|
||||||
.replaceAll("{added_month}", "12")
|
|
||||||
.replaceAll("{added_day}", "24");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resetfield(FormFieldState<String> field) {
|
void _resetfield(FormFieldState<String> field) {
|
||||||
|
|||||||
@@ -45,11 +45,13 @@ class StoragePathWidget extends StatelessWidget {
|
|||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
if (cubit.state.filter.correspondent.id == pathId) {
|
if (cubit.state.filter.correspondent.id == pathId) {
|
||||||
cubit.updateCurrentFilter(
|
cubit.updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(storagePath: const StoragePathQuery.unset()),
|
(filter) =>
|
||||||
|
filter.copyWith(storagePath: const StoragePathQuery.unset()),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cubit.updateCurrentFilter(
|
cubit.updateCurrentFilter(
|
||||||
(filter) => filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)),
|
(filter) =>
|
||||||
|
filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
afterSelected?.call();
|
afterSelected?.call();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:injectable/injectable.dart';
|
|||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class TagCubit extends LabelCubit<Tag> {
|
class TagCubit extends LabelCubit<Tag> {
|
||||||
TagCubit(super.metaDataService);
|
TagCubit(super.metaDataService, super.errorCubit);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
@@ -85,7 +85,8 @@ String? _toHex(Color? color) {
|
|||||||
if (color == null) {
|
if (color == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String val = '#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}';
|
String val =
|
||||||
|
'#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}';
|
||||||
log("Color in Tag#_toHex is $val");
|
log("Color in Tag#_toHex is $val");
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class AddTagPage extends StatelessWidget {
|
|||||||
label: Text(S.of(context).tagColorPropertyLabel),
|
label: Text(S.of(context).tagColorPropertyLabel),
|
||||||
),
|
),
|
||||||
colorPickerType: ColorPickerType.materialPicker,
|
colorPickerType: ColorPickerType.materialPicker,
|
||||||
|
initialValue: null,
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
FormBuilderCheckbox(
|
||||||
name: Tag.isInboxTagKey,
|
name: Tag.isInboxTagKey,
|
||||||
|
|||||||
@@ -43,23 +43,18 @@ class EditTagPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDelete(Tag tag, BuildContext context) async {
|
Future<void> _onDelete(Tag tag, BuildContext context) async {
|
||||||
try {
|
await BlocProvider.of<TagCubit>(context).remove(tag);
|
||||||
await BlocProvider.of<TagCubit>(context).remove(tag);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
final currentFilter = cubit.state.filter;
|
||||||
final currentFilter = cubit.state.filter;
|
late DocumentFilter updatedFilter = currentFilter;
|
||||||
late DocumentFilter updatedFilter = currentFilter;
|
if (currentFilter.tags.ids.contains(tag.id)) {
|
||||||
if (currentFilter.tags.ids.contains(tag.id)) {
|
updatedFilter = currentFilter.copyWith(
|
||||||
updatedFilter = currentFilter.copyWith(
|
tags: TagsQuery.fromIds(
|
||||||
tags: TagsQuery.fromIds(
|
currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList(),
|
||||||
currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList(),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
cubit.updateFilter(filter: updatedFilter);
|
|
||||||
} on ErrorMessage catch (error) {
|
|
||||||
showError(context, error);
|
|
||||||
} finally {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
}
|
||||||
|
cubit.updateFilter(filter: updatedFilter);
|
||||||
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
|
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||||
|
import 'package:paperless_mobile/util.dart';
|
||||||
|
|
||||||
class TagWidget extends StatelessWidget {
|
class TagWidget extends StatelessWidget {
|
||||||
final Tag tag;
|
final Tag tag;
|
||||||
@@ -37,15 +39,17 @@ class TagWidget extends StatelessWidget {
|
|||||||
void _addTagToFilter(BuildContext context) {
|
void _addTagToFilter(BuildContext context) {
|
||||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||||
if (cubit.state.filter.tags.ids.contains(tag.id)) {
|
if (cubit.state.filter.tags.ids.contains(tag.id)) {
|
||||||
cubit.updateFilter(
|
cubit.updateCurrentFilter(
|
||||||
filter: cubit.state.filter.copyWith(
|
(filter) => filter.copyWith(
|
||||||
tags: TagsQuery.fromIds(cubit.state.filter.tags.ids.where((id) => id != tag.id).toList()),
|
tags: TagsQuery.fromIds(
|
||||||
|
cubit.state.filter.tags.ids.where((id) => id != tag.id).toList()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cubit.updateFilter(
|
cubit.updateCurrentFilter(
|
||||||
filter: cubit.state.filter
|
(filter) => filter.copyWith(
|
||||||
.copyWith(tags: TagsQuery.fromIds([...cubit.state.filter.tags.ids, tag.id!])),
|
tags: TagsQuery.fromIds([...cubit.state.filter.tags.ids, tag.id!]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (afterTagTapped != null) {
|
if (afterTagTapped != null) {
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
selectedColor: tag.color,
|
selectedColor: tag.color,
|
||||||
selected: field.value?.ids.contains(tag.id) ?? false,
|
selected:
|
||||||
|
field.value?.ids.contains(tag.id) ?? false,
|
||||||
onSelected: (isSelected) {
|
onSelected: (isSelected) {
|
||||||
List<int> ids = [...field.value?.ids ?? []];
|
List<int> ids = [...field.value?.ids ?? []];
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ class TagsWidget extends StatefulWidget {
|
|||||||
final List<int> tagIds;
|
final List<int> tagIds;
|
||||||
final bool isMultiLine;
|
final bool isMultiLine;
|
||||||
final void Function()? afterTagTapped;
|
final void Function()? afterTagTapped;
|
||||||
|
final bool isClickable;
|
||||||
|
|
||||||
const TagsWidget({
|
const TagsWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.tagIds,
|
required this.tagIds,
|
||||||
this.afterTagTapped,
|
this.afterTagTapped,
|
||||||
this.isMultiLine = true,
|
this.isMultiLine = true,
|
||||||
|
this.isClickable = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|||||||
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
@@ -34,7 +35,7 @@ class AddLabelPage<T extends Label> extends StatefulWidget {
|
|||||||
|
|
||||||
class _AddLabelPageState<T extends Label> extends State<AddLabelPage<T>> {
|
class _AddLabelPageState<T extends Label> extends State<AddLabelPage<T>> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
Map<String, String> _errors = {};
|
PaperlessValidationErrors _errors = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -103,11 +104,12 @@ class _AddLabelPageState<T extends Label> extends State<AddLabelPage<T>> {
|
|||||||
void _onSubmit() async {
|
void _onSubmit() async {
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
try {
|
try {
|
||||||
final label = await widget.cubit.add(widget.fromJson(_formKey.currentState!.value));
|
final label = await widget.cubit
|
||||||
|
.add(widget.fromJson(_formKey.currentState!.value));
|
||||||
Navigator.pop(context, label);
|
Navigator.pop(context, label);
|
||||||
} on ErrorMessage catch (e) {
|
} on ErrorMessage catch (e) {
|
||||||
showSnackBar(context, translateError(context, e.code));
|
showSnackBar(context, translateError(context, e.code));
|
||||||
} on Map<String, String> catch (json) {
|
} on PaperlessValidationErrors catch (json) {
|
||||||
setState(() => _errors = json);
|
setState(() => _errors = json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
@@ -35,7 +35,7 @@ class EditLabelPage<T extends Label> extends StatefulWidget {
|
|||||||
class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
|
class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
|
||||||
Map<String, String> _errors = {};
|
PaperlessValidationErrors _errors = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -80,8 +80,8 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
|
|||||||
),
|
),
|
||||||
FormBuilderDropdown<int?>(
|
FormBuilderDropdown<int?>(
|
||||||
name: Label.matchingAlgorithmKey,
|
name: Label.matchingAlgorithmKey,
|
||||||
initialValue:
|
initialValue: widget.label.matchingAlgorithm?.value ??
|
||||||
widget.label.matchingAlgorithm?.value ?? MatchingAlgorithm.allWords.value,
|
MatchingAlgorithm.allWords.value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: S.of(context).labelMatchingAlgorithmPropertyLabel,
|
labelText: S.of(context).labelMatchingAlgorithmPropertyLabel,
|
||||||
errorText: _errors[Label.matchingAlgorithmKey],
|
errorText: _errors[Label.matchingAlgorithmKey],
|
||||||
@@ -111,12 +111,13 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
|
|||||||
void _onSubmit() async {
|
void _onSubmit() async {
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
try {
|
try {
|
||||||
final mergedJson = {...widget.label.toJson(), ..._formKey.currentState!.value};
|
final mergedJson = {
|
||||||
|
...widget.label.toJson(),
|
||||||
|
..._formKey.currentState!.value
|
||||||
|
};
|
||||||
await widget.onSubmit(widget.fromJson(mergedJson));
|
await widget.onSubmit(widget.fromJson(mergedJson));
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
} on ErrorMessage catch (e) {
|
} on PaperlessValidationErrors catch (errorMessages) {
|
||||||
showSnackBar(context, translateError(context, e.code));
|
|
||||||
} on Map<String, String> catch (errorMessages) {
|
|
||||||
setState(() => _errors = errorMessages);
|
setState(() => _errors = errorMessages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ class LabelsPage extends StatefulWidget {
|
|||||||
State<LabelsPage> createState() => _LabelsPageState();
|
State<LabelsPage> createState() => _LabelsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateMixin {
|
class _LabelsPageState extends State<LabelsPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late final TabController _tabController;
|
late final TabController _tabController;
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
||||||
@@ -54,100 +55,126 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return BlocProvider.value(
|
||||||
length: 3,
|
value: getIt<DocumentsCubit>(),
|
||||||
child: Scaffold(
|
child: DefaultTabController(
|
||||||
drawer: const InfoDrawer(),
|
length: 3,
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
title: Text(
|
drawer: const InfoDrawer(),
|
||||||
[
|
appBar: AppBar(
|
||||||
S.of(context).labelsPageCorrespondentsTitleText,
|
title: Text(
|
||||||
S.of(context).labelsPageDocumentTypesTitleText,
|
[
|
||||||
S.of(context).labelsPageTagsTitleText,
|
S.of(context).labelsPageCorrespondentsTitleText,
|
||||||
S.of(context).labelsPageStoragePathTitleText
|
S.of(context).labelsPageDocumentTypesTitleText,
|
||||||
][_currentIndex],
|
S.of(context).labelsPageTagsTitleText,
|
||||||
),
|
S.of(context).labelsPageStoragePathTitleText
|
||||||
actions: [
|
][_currentIndex],
|
||||||
IconButton(
|
),
|
||||||
onPressed: _onAddPressed,
|
actions: [
|
||||||
icon: const Icon(Icons.add),
|
IconButton(
|
||||||
)
|
onPressed: _onAddPressed,
|
||||||
],
|
icon: const Icon(Icons.add),
|
||||||
bottom: PreferredSize(
|
)
|
||||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
],
|
||||||
child: ColoredBox(
|
bottom: PreferredSize(
|
||||||
color: Theme.of(context).bottomAppBarColor,
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
child: TabBar(
|
child: ColoredBox(
|
||||||
indicatorColor: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).bottomAppBarColor,
|
||||||
controller: _tabController,
|
child: TabBar(
|
||||||
tabs: [
|
indicatorColor: Theme.of(context).colorScheme.primary,
|
||||||
Tab(
|
controller: _tabController,
|
||||||
icon: Icon(
|
tabs: [
|
||||||
Icons.person_outline,
|
Tab(
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
icon: Icon(
|
||||||
|
Icons.person_outline,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Tab(
|
||||||
Tab(
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.description_outlined,
|
||||||
Icons.description_outlined,
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
),
|
||||||
),
|
),
|
||||||
),
|
Tab(
|
||||||
Tab(
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.label_outline,
|
||||||
Icons.label_outline,
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
),
|
||||||
),
|
),
|
||||||
),
|
Tab(
|
||||||
Tab(
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.folder_open,
|
||||||
Icons.folder_open,
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
),
|
||||||
),
|
)
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
body: TabBarView(
|
||||||
body: TabBarView(
|
controller: _tabController,
|
||||||
controller: _tabController,
|
children: [
|
||||||
children: [
|
LabelTabView<Correspondent>(
|
||||||
LabelTabView<Correspondent>(
|
cubit: BlocProvider.of<CorrespondentCubit>(context),
|
||||||
cubit: BlocProvider.of<CorrespondentCubit>(context),
|
filterBuilder: (label) => DocumentFilter(
|
||||||
filterBuilder: (label) => DocumentFilter(
|
correspondent: CorrespondentQuery.fromId(label.id),
|
||||||
correspondent: CorrespondentQuery.fromId(label.id),
|
pageSize: label.documentCount ?? 0,
|
||||||
pageSize: label.documentCount ?? 0,
|
),
|
||||||
|
onOpenEditPage: _openEditCorrespondentPage,
|
||||||
|
emptyStateActionButtonLabel:
|
||||||
|
S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel,
|
||||||
|
emptyStateDescription: S
|
||||||
|
.of(context)
|
||||||
|
.labelsPageCorrespondentEmptyStateDescriptionText,
|
||||||
|
onOpenAddNewPage: _onAddPressed,
|
||||||
),
|
),
|
||||||
onOpenEditPage: _openEditCorrespondentPage,
|
LabelTabView<DocumentType>(
|
||||||
),
|
cubit: BlocProvider.of<DocumentTypeCubit>(context),
|
||||||
LabelTabView<DocumentType>(
|
filterBuilder: (label) => DocumentFilter(
|
||||||
cubit: BlocProvider.of<DocumentTypeCubit>(context),
|
documentType: DocumentTypeQuery.fromId(label.id),
|
||||||
filterBuilder: (label) => DocumentFilter(
|
pageSize: label.documentCount ?? 0,
|
||||||
documentType: DocumentTypeQuery.fromId(label.id),
|
),
|
||||||
pageSize: label.documentCount ?? 0,
|
onOpenEditPage: _openEditDocumentTypePage,
|
||||||
|
emptyStateActionButtonLabel:
|
||||||
|
S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel,
|
||||||
|
emptyStateDescription: S
|
||||||
|
.of(context)
|
||||||
|
.labelsPageDocumentTypeEmptyStateDescriptionText,
|
||||||
|
onOpenAddNewPage: _onAddPressed,
|
||||||
),
|
),
|
||||||
onOpenEditPage: _openEditDocumentTypePage,
|
LabelTabView<Tag>(
|
||||||
),
|
cubit: BlocProvider.of<TagCubit>(context),
|
||||||
LabelTabView<Tag>(
|
filterBuilder: (label) => DocumentFilter(
|
||||||
cubit: BlocProvider.of<TagCubit>(context),
|
tags: TagsQuery.fromIds([label.id!]),
|
||||||
filterBuilder: (label) => DocumentFilter(
|
pageSize: label.documentCount ?? 0,
|
||||||
tags: TagsQuery.fromIds([label.id!]),
|
),
|
||||||
pageSize: label.documentCount ?? 0,
|
onOpenEditPage: _openEditTagPage,
|
||||||
|
leadingBuilder: (t) => CircleAvatar(backgroundColor: t.color),
|
||||||
|
emptyStateActionButtonLabel:
|
||||||
|
S.of(context).labelsPageTagsEmptyStateAddNewLabel,
|
||||||
|
emptyStateDescription:
|
||||||
|
S.of(context).labelsPageTagsEmptyStateDescriptionText,
|
||||||
|
onOpenAddNewPage: _onAddPressed,
|
||||||
),
|
),
|
||||||
onOpenEditPage: _openEditTagPage,
|
LabelTabView<StoragePath>(
|
||||||
leadingBuilder: (t) => CircleAvatar(backgroundColor: t.color),
|
cubit: BlocProvider.of<StoragePathCubit>(context),
|
||||||
),
|
onOpenEditPage: _openEditStoragePathPage,
|
||||||
LabelTabView<StoragePath>(
|
filterBuilder: (label) => DocumentFilter(
|
||||||
cubit: BlocProvider.of<StoragePathCubit>(context),
|
storagePath: StoragePathQuery.fromId(label.id),
|
||||||
onOpenEditPage: _openEditStoragePathPage,
|
pageSize: label.documentCount ?? 0,
|
||||||
filterBuilder: (label) => DocumentFilter(
|
),
|
||||||
storagePath: StoragePathQuery.fromId(label.id),
|
contentBuilder: (path) => Text(path.path ?? ""),
|
||||||
pageSize: label.documentCount ?? 0,
|
emptyStateActionButtonLabel:
|
||||||
|
S.of(context).labelsPageStoragePathEmptyStateAddNewLabel,
|
||||||
|
emptyStateDescription: S
|
||||||
|
.of(context)
|
||||||
|
.labelsPageStoragePathEmptyStateDescriptionText,
|
||||||
|
onOpenAddNewPage: _onAddPressed,
|
||||||
),
|
),
|
||||||
contentBuilder: (path) => Text(path.path ?? ""),
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -160,7 +187,8 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
builder: (_) => MultiBlocProvider(
|
builder: (_) => MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||||
BlocProvider.value(value: BlocProvider.of<CorrespondentCubit>(context)),
|
BlocProvider.value(
|
||||||
|
value: BlocProvider.of<CorrespondentCubit>(context)),
|
||||||
],
|
],
|
||||||
child: EditCorrespondentPage(correspondent: correspondent),
|
child: EditCorrespondentPage(correspondent: correspondent),
|
||||||
),
|
),
|
||||||
@@ -175,7 +203,8 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
builder: (_) => MultiBlocProvider(
|
builder: (_) => MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||||
BlocProvider.value(value: BlocProvider.of<DocumentTypeCubit>(context)),
|
BlocProvider.value(
|
||||||
|
value: BlocProvider.of<DocumentTypeCubit>(context)),
|
||||||
],
|
],
|
||||||
child: EditDocumentTypePage(documentType: docType),
|
child: EditDocumentTypePage(documentType: docType),
|
||||||
),
|
),
|
||||||
@@ -205,7 +234,8 @@ class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateM
|
|||||||
builder: (_) => MultiBlocProvider(
|
builder: (_) => MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||||
BlocProvider.value(value: BlocProvider.of<StoragePathCubit>(context)),
|
BlocProvider.value(
|
||||||
|
value: BlocProvider.of<StoragePathCubit>(context)),
|
||||||
],
|
],
|
||||||
child: EditStoragePathPage(storagePath: path),
|
child: EditStoragePathPage(storagePath: path),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
|
|||||||
/// Form field allowing to select labels (i.e. correspondent, documentType)
|
/// Form field allowing to select labels (i.e. correspondent, documentType)
|
||||||
/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...), [R] is the return type (e.g. [CorrespondentQuery], ...).
|
/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...), [R] is the return type (e.g. [CorrespondentQuery], ...).
|
||||||
///
|
///
|
||||||
class LabelFormField<T extends Label, R extends IdQueryParameter> extends StatefulWidget {
|
class LabelFormField<T extends Label, R extends IdQueryParameter>
|
||||||
|
extends StatefulWidget {
|
||||||
final Widget prefixIcon;
|
final Widget prefixIcon;
|
||||||
final Map<int, T> state;
|
final Map<int, T> state;
|
||||||
final FormBuilderState? formBuilderState;
|
final FormBuilderState? formBuilderState;
|
||||||
@@ -57,18 +58,19 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_showClearSuffixIcon = widget.state.containsKey(widget.initialValue?.id);
|
_showClearSuffixIcon = widget.state.containsKey(widget.initialValue?.id);
|
||||||
_textEditingController =
|
_textEditingController = TextEditingController(
|
||||||
TextEditingController(text: widget.state[widget.initialValue?.id]?.name ?? '')
|
text: widget.state[widget.initialValue?.id]?.name ?? '')
|
||||||
..addListener(() {
|
..addListener(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showCreationSuffixIcon = widget.state.values
|
_showCreationSuffixIcon = widget.state.values
|
||||||
.where((item) => item.name.toLowerCase().startsWith(
|
.where((item) => item.name.toLowerCase().startsWith(
|
||||||
_textEditingController.text.toLowerCase(),
|
_textEditingController.text.toLowerCase(),
|
||||||
))
|
))
|
||||||
.isEmpty;
|
.isEmpty;
|
||||||
});
|
});
|
||||||
setState(() => _showClearSuffixIcon = _textEditingController.text.isNotEmpty);
|
setState(() =>
|
||||||
});
|
_showClearSuffixIcon = _textEditingController.text.isNotEmpty);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -79,18 +81,22 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
|
|||||||
child: Text(
|
child: Text(
|
||||||
S.of(context).labelFormFieldNoItemsFoundText,
|
S.of(context).labelFormFieldNoItemsFoundText,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0),
|
style:
|
||||||
|
TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
initialValue: widget.initialValue ?? widget.queryParameterIdBuilder(null),
|
initialValue: widget.initialValue ?? widget.queryParameterIdBuilder(null),
|
||||||
name: widget.name,
|
name: widget.name,
|
||||||
itemBuilder: (context, suggestion) => ListTile(
|
itemBuilder: (context, suggestion) => ListTile(
|
||||||
title: Text(widget.state[suggestion.id]?.name ?? S.of(context).labelNotAssignedText),
|
title: Text(widget.state[suggestion.id]?.name ??
|
||||||
|
S.of(context).labelNotAssignedText),
|
||||||
),
|
),
|
||||||
suggestionsCallback: (pattern) {
|
suggestionsCallback: (pattern) {
|
||||||
final List<IdQueryParameter> suggestions = widget.state.keys
|
final List<IdQueryParameter> suggestions = widget.state.keys
|
||||||
.where((item) =>
|
.where((item) =>
|
||||||
widget.state[item]!.name.toLowerCase().startsWith(pattern.toLowerCase()) ||
|
widget.state[item]!.name
|
||||||
|
.toLowerCase()
|
||||||
|
.startsWith(pattern.toLowerCase()) ||
|
||||||
pattern.isEmpty)
|
pattern.isEmpty)
|
||||||
.map((id) => widget.queryParameterIdBuilder(id))
|
.map((id) => widget.queryParameterIdBuilder(id))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -117,8 +123,9 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
|
|||||||
return widget.state[suggestion.id]?.name ?? "";
|
return widget.state[suggestion.id]?.name ?? "";
|
||||||
},
|
},
|
||||||
direction: AxisDirection.up,
|
direction: AxisDirection.up,
|
||||||
onSuggestionSelected: (suggestion) =>
|
onSuggestionSelected: (suggestion) => widget
|
||||||
widget.formBuilderState?.fields[widget.name]?.didChange(suggestion as R),
|
.formBuilderState?.fields[widget.name]
|
||||||
|
?.didChange(suggestion as R),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +134,8 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () => Navigator.of(context)
|
onPressed: () => Navigator.of(context)
|
||||||
.push<T>(MaterialPageRoute(
|
.push<T>(MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => widget
|
||||||
widget.labelCreationWidgetBuilder!(_textEditingController.text)))
|
.labelCreationWidgetBuilder!(_textEditingController.text)))
|
||||||
.then((value) {
|
.then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
// If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done).
|
// If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done).
|
||||||
@@ -155,7 +162,8 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _reset() {
|
void _reset() {
|
||||||
widget.formBuilderState?.fields[widget.name]?.didChange(widget.queryParameterIdBuilder(null));
|
widget.formBuilderState?.fields[widget.name]
|
||||||
|
?.didChange(widget.queryParameterIdBuilder(null));
|
||||||
_textEditingController.clear();
|
_textEditingController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
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/global_error_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
|
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
|
||||||
|
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
||||||
@@ -33,11 +36,11 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
|||||||
subtitle: content,
|
subtitle: content,
|
||||||
leading: leading,
|
leading: leading,
|
||||||
onTap: () => onOpenEditPage(label),
|
onTap: () => onOpenEditPage(label),
|
||||||
trailing: _buildDocumentCountWidget(context),
|
trailing: _buildReferencedDocumentsWidget(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentCountWidget(BuildContext context) {
|
Widget _buildReferencedDocumentsWidget(BuildContext context) {
|
||||||
return TextButton.icon(
|
return TextButton.icon(
|
||||||
label: const Icon(Icons.link),
|
label: const Icon(Icons.link),
|
||||||
icon: Text(_formatDocumentCount(label.documentCount)),
|
icon: Text(_formatDocumentCount(label.documentCount)),
|
||||||
@@ -50,8 +53,10 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => LabelBlocProvider(
|
builder: (context) => LabelBlocProvider(
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) =>
|
create: (context) => DocumentsCubit(
|
||||||
DocumentsCubit(getIt<DocumentRepository>())..updateFilter(filter: filter),
|
getIt<DocumentRepository>(),
|
||||||
|
getIt<GlobalErrorCubit>())
|
||||||
|
..updateFilter(filter: filter),
|
||||||
child: LinkedDocumentsPreview(filter: filter),
|
child: LinkedDocumentsPreview(filter: filter),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
|
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/repository/document_repository.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/linked_documents_preview.dart';
|
|
||||||
|
|
||||||
class LabelListTile<T extends Label> extends StatelessWidget {
|
|
||||||
final T label;
|
|
||||||
final DocumentFilter Function(Label) filterBuilder;
|
|
||||||
final void Function() onOpenEditPage;
|
|
||||||
|
|
||||||
const LabelListTile(
|
|
||||||
this.label, {
|
|
||||||
super.key,
|
|
||||||
required this.filterBuilder,
|
|
||||||
required this.onOpenEditPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
leading: (label is Tag)
|
|
||||||
? CircleAvatar(
|
|
||||||
backgroundColor: (label as Tag).color,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
title: Text(label.name),
|
|
||||||
onTap: onOpenEditPage,
|
|
||||||
trailing: _buildDocumentCountWidget(context),
|
|
||||||
subtitle: Text(
|
|
||||||
(label.match?.isEmpty ?? true) ? "-" : label.match!,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDocumentCountWidget(BuildContext context) {
|
|
||||||
return TextButton.icon(
|
|
||||||
label: const Icon(Icons.link),
|
|
||||||
icon: Text(_formatDocumentCount(label.documentCount)),
|
|
||||||
onPressed: (label.documentCount ?? 0) == 0
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
final filter = filterBuilder(label);
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => LabelBlocProvider(
|
|
||||||
child: BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
DocumentsCubit(getIt<DocumentRepository>())..updateFilter(filter: filter),
|
|
||||||
child: LinkedDocumentsPreview(filter: filter),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatDocumentCount(int? count) {
|
|
||||||
if ((count ?? 0) > 99) {
|
|
||||||
return "99+";
|
|
||||||
}
|
|
||||||
return (count ?? 0).toString().padLeft(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
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/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
|
||||||
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
import 'package:paperless_mobile/features/labels/model/label.model.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
class LabelTabView<T extends Label> extends StatelessWidget {
|
class LabelTabView<T extends Label> extends StatelessWidget {
|
||||||
final LabelCubit<T> cubit;
|
final LabelCubit<T> cubit;
|
||||||
final DocumentFilter Function(Label) filterBuilder;
|
final DocumentFilter Function(Label) filterBuilder;
|
||||||
final void Function(T) onOpenEditPage;
|
final void Function(T) onOpenEditPage;
|
||||||
|
final void Function() onOpenAddNewPage;
|
||||||
|
|
||||||
/// Displayed as the subtitle of the [ListTile]
|
/// Displayed as the subtitle of the [ListTile]
|
||||||
final Widget Function(T)? contentBuilder;
|
final Widget Function(T)? contentBuilder;
|
||||||
@@ -16,6 +20,10 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
|||||||
/// Displayed as the leading widget of the [ListTile]
|
/// Displayed as the leading widget of the [ListTile]
|
||||||
final Widget Function(T)? leadingBuilder;
|
final Widget Function(T)? leadingBuilder;
|
||||||
|
|
||||||
|
/// Shown on empty State
|
||||||
|
final String emptyStateDescription;
|
||||||
|
final String emptyStateActionButtonLabel;
|
||||||
|
|
||||||
const LabelTabView({
|
const LabelTabView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.cubit,
|
required this.cubit,
|
||||||
@@ -23,27 +31,55 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
|||||||
this.contentBuilder,
|
this.contentBuilder,
|
||||||
this.leadingBuilder,
|
this.leadingBuilder,
|
||||||
required this.onOpenEditPage,
|
required this.onOpenEditPage,
|
||||||
|
required this.emptyStateDescription,
|
||||||
|
required this.onOpenAddNewPage,
|
||||||
|
required this.emptyStateActionButtonLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<Cubit<Map<int, T>>, Map<int, T>>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
bloc: cubit,
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final labels = state.values.toList()..sort();
|
if (state == ConnectivityState.notConnected) {
|
||||||
|
return const OfflineWidget();
|
||||||
|
}
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: cubit.initialize,
|
onRefresh: cubit.initialize,
|
||||||
child: ListView(
|
child: BlocBuilder<Cubit<Map<int, T>>, Map<int, T>>(
|
||||||
children: labels
|
bloc: cubit,
|
||||||
.map((l) => LabelItem<T>(
|
builder: (context, state) {
|
||||||
name: l.name,
|
final labels = state.values.toList()..sort();
|
||||||
content: contentBuilder?.call(l) ?? Text(l.match ?? '-'),
|
if (labels.isEmpty) {
|
||||||
onOpenEditPage: onOpenEditPage,
|
return Center(
|
||||||
filterBuilder: filterBuilder,
|
child: Column(
|
||||||
leading: leadingBuilder?.call(l),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
label: l,
|
children: [
|
||||||
))
|
Text(
|
||||||
.toList(),
|
emptyStateDescription,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: onOpenAddNewPage,
|
||||||
|
child: Text(emptyStateActionButtonLabel),
|
||||||
|
)
|
||||||
|
].padded(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView(
|
||||||
|
children: labels
|
||||||
|
.map((l) => LabelItem<T>(
|
||||||
|
name: l.name,
|
||||||
|
content:
|
||||||
|
contentBuilder?.call(l) ?? Text(l.match ?? '-'),
|
||||||
|
onOpenEditPage: onOpenEditPage,
|
||||||
|
filterBuilder: filterBuilder,
|
||||||
|
leading: leadingBuilder?.call(l),
|
||||||
|
label: l,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class LinkedDocumentsPreview extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LinkedDocumentsPreviewState extends State<LinkedDocumentsPreview> {
|
class _LinkedDocumentsPreviewState extends State<LinkedDocumentsPreview> {
|
||||||
final PagingController<int, DocumentModel> _pagingController = PagingController(firstPageKey: 1);
|
final _pagingController =
|
||||||
|
PagingController<int, DocumentModel>(firstPageKey: 1);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -37,25 +38,43 @@ class _LinkedDocumentsPreviewState extends State<LinkedDocumentsPreview> {
|
|||||||
body: BlocBuilder<DocumentsCubit, DocumentsState>(
|
body: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
_pagingController.itemList = state.documents;
|
_pagingController.itemList = state.documents;
|
||||||
return CustomScrollView(
|
return Column(
|
||||||
slivers: [
|
children: [
|
||||||
DocumentListView(
|
Text(
|
||||||
onTap: (doc) {
|
S.of(context).referencedDocumentsReadOnlyHintText,
|
||||||
Navigator.push(
|
textAlign: TextAlign.center,
|
||||||
context,
|
style: Theme.of(context).textTheme.caption,
|
||||||
MaterialPageRoute(
|
),
|
||||||
builder: (ctxt) => LabelBlocProvider(
|
Expanded(
|
||||||
child: BlocProvider.value(
|
child: CustomScrollView(
|
||||||
value: BlocProvider.of<DocumentsCubit>(context),
|
slivers: [
|
||||||
child: DocumentDetailsPage(documentId: doc.id)),
|
DocumentListView(
|
||||||
),
|
isLabelClickable: false,
|
||||||
|
onTap: (doc) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctxt) => LabelBlocProvider(
|
||||||
|
child: BlocProvider.value(
|
||||||
|
value: BlocProvider.of<DocumentsCubit>(context),
|
||||||
|
child: DocumentDetailsPage(
|
||||||
|
documentId: doc.id,
|
||||||
|
allowEdit: false,
|
||||||
|
isLabelClickable: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pagingController: _pagingController,
|
||||||
|
state: state,
|
||||||
|
onSelected: BlocProvider.of<DocumentsCubit>(context)
|
||||||
|
.toggleDocumentSelection,
|
||||||
|
hasInternetConnection: true,
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
pagingController: _pagingController,
|
|
||||||
state: state,
|
|
||||||
onSelected: BlocProvider.of<DocumentsCubit>(context).toggleDocumentSelection,
|
|
||||||
hasInternetConnection: true,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
@@ -16,10 +17,14 @@ const authenticationKey = "authentication";
|
|||||||
@singleton
|
@singleton
|
||||||
class AuthenticationCubit extends Cubit<AuthenticationState> {
|
class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||||
final LocalVault localStore;
|
final LocalVault localStore;
|
||||||
|
final GlobalErrorCubit errorCubit;
|
||||||
final AuthenticationService authenticationService;
|
final AuthenticationService authenticationService;
|
||||||
|
|
||||||
AuthenticationCubit(this.localStore, this.authenticationService)
|
AuthenticationCubit(
|
||||||
: super(AuthenticationState.initial);
|
this.localStore,
|
||||||
|
this.authenticationService,
|
||||||
|
this.errorCubit,
|
||||||
|
) : super(AuthenticationState.initial);
|
||||||
|
|
||||||
Future<void> initialize() {
|
Future<void> initialize() {
|
||||||
return restoreSessionState();
|
return restoreSessionState();
|
||||||
@@ -29,68 +34,90 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
required UserCredentials credentials,
|
required UserCredentials credentials,
|
||||||
required String serverUrl,
|
required String serverUrl,
|
||||||
ClientCertificate? clientCertificate,
|
ClientCertificate? clientCertificate,
|
||||||
|
bool propagateEventOnError = true,
|
||||||
}) async {
|
}) async {
|
||||||
assert(credentials.username != null && credentials.password != null);
|
assert(credentials.username != null && credentials.password != null);
|
||||||
try {
|
try {
|
||||||
registerSecurityContext(clientCertificate);
|
registerSecurityContext(clientCertificate);
|
||||||
} on TlsException catch (_) {
|
emit(
|
||||||
throw const ErrorMessage(ErrorCode.invalidClientCertificateConfiguration);
|
AuthenticationState(
|
||||||
}
|
isAuthenticated: false,
|
||||||
emit(
|
wasLoginStored: false,
|
||||||
AuthenticationState(
|
authentication: AuthenticationInformation(
|
||||||
isAuthenticated: false,
|
username: credentials.username!,
|
||||||
wasLoginStored: false,
|
password: credentials.password!,
|
||||||
authentication: AuthenticationInformation(
|
serverUrl: serverUrl,
|
||||||
username: credentials.username!,
|
token: "",
|
||||||
password: credentials.password!,
|
clientCertificate: clientCertificate,
|
||||||
serverUrl: serverUrl,
|
),
|
||||||
token: "",
|
|
||||||
clientCertificate: clientCertificate,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
final token = await authenticationService.login(
|
||||||
final token = await authenticationService.login(
|
username: credentials.username!,
|
||||||
username: credentials.username!,
|
password: credentials.password!,
|
||||||
password: credentials.password!,
|
serverUrl: serverUrl,
|
||||||
serverUrl: serverUrl,
|
);
|
||||||
);
|
final auth = AuthenticationInformation(
|
||||||
final auth = AuthenticationInformation(
|
username: credentials.username!,
|
||||||
username: credentials.username!,
|
password: credentials.password!,
|
||||||
password: credentials.password!,
|
token: token,
|
||||||
token: token,
|
serverUrl: serverUrl,
|
||||||
serverUrl: serverUrl,
|
clientCertificate: clientCertificate,
|
||||||
clientCertificate: clientCertificate,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
await localStore.storeAuthenticationInformation(auth);
|
await localStore.storeAuthenticationInformation(auth);
|
||||||
|
|
||||||
emit(AuthenticationState(
|
emit(AuthenticationState(
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
wasLoginStored: false,
|
wasLoginStored: false,
|
||||||
authentication: auth,
|
authentication: auth,
|
||||||
));
|
));
|
||||||
|
} on TlsException catch (_) {
|
||||||
|
const error =
|
||||||
|
ErrorMessage(ErrorCode.invalidClientCertificateConfiguration);
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreSessionState() async {
|
Future<void> restoreSessionState({
|
||||||
final storedAuth = await localStore.loadAuthenticationInformation();
|
bool propagateEventOnError = true,
|
||||||
final appSettings =
|
}) async {
|
||||||
await localStore.loadApplicationSettings() ?? ApplicationSettingsState.defaultSettings;
|
try {
|
||||||
|
final storedAuth = await localStore.loadAuthenticationInformation();
|
||||||
|
final appSettings = await localStore.loadApplicationSettings() ??
|
||||||
|
ApplicationSettingsState.defaultSettings;
|
||||||
|
|
||||||
if (storedAuth == null || !storedAuth.isValid) {
|
if (storedAuth == null || !storedAuth.isValid) {
|
||||||
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false));
|
|
||||||
} else {
|
|
||||||
if (!appSettings.isLocalAuthenticationEnabled ||
|
|
||||||
await authenticationService.authenticateLocalUser("Authenticate to log back in")) {
|
|
||||||
registerSecurityContext(storedAuth.clientCertificate);
|
|
||||||
emit(
|
emit(
|
||||||
AuthenticationState(
|
AuthenticationState(isAuthenticated: false, wasLoginStored: false));
|
||||||
isAuthenticated: true,
|
|
||||||
wasLoginStored: true,
|
|
||||||
authentication: storedAuth,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
|
if (!appSettings.isLocalAuthenticationEnabled ||
|
||||||
|
await authenticationService
|
||||||
|
.authenticateLocalUser("Authenticate to log back in")) {
|
||||||
|
registerSecurityContext(storedAuth.clientCertificate);
|
||||||
|
emit(
|
||||||
|
AuthenticationState(
|
||||||
|
isAuthenticated: true,
|
||||||
|
wasLoginStored: true,
|
||||||
|
authentication: storedAuth,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(AuthenticationState(
|
||||||
|
isAuthenticated: false, wasLoginStored: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on ErrorMessage catch (error) {
|
||||||
|
if (propagateEventOnError) {
|
||||||
|
errorCubit.add(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ class LocalAuthenticationCubit extends Cubit<LocalAuthenticationState> {
|
|||||||
LocalAuthenticationCubit() : super(LocalAuthenticationState(false));
|
LocalAuthenticationCubit() : super(LocalAuthenticationState(false));
|
||||||
|
|
||||||
Future<void> authorize(String localizedMessage) async {
|
Future<void> authorize(String localizedMessage) async {
|
||||||
final isAuthenticationSuccessful =
|
final isAuthenticationSuccessful = await getIt<LocalAuthentication>()
|
||||||
await getIt<LocalAuthentication>().authenticate(localizedReason: localizedMessage);
|
.authenticate(localizedReason: localizedMessage);
|
||||||
if (isAuthenticationSuccessful) {
|
if (isAuthenticationSuccessful) {
|
||||||
emit(LocalAuthenticationState(true));
|
emit(LocalAuthenticationState(true));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
|
||||||
class AuthenticationInformation {
|
class AuthenticationInformation {
|
||||||
@@ -59,8 +59,8 @@ class AuthenticationInformation {
|
|||||||
password: password ?? this.password,
|
password: password ?? this.password,
|
||||||
token: token ?? this.token,
|
token: token ?? this.token,
|
||||||
serverUrl: serverUrl ?? this.serverUrl,
|
serverUrl: serverUrl ?? this.serverUrl,
|
||||||
clientCertificate:
|
clientCertificate: clientCertificate ??
|
||||||
clientCertificate ?? (removeClientCertificate ? null : this.clientCertificate),
|
(removeClientCertificate ? null : this.clientCertificate),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:paperless_mobile/core/type/json.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
|
|
||||||
class ClientCertificate {
|
class ClientCertificate {
|
||||||
static const bytesKey = 'bytes';
|
static const bytesKey = 'bytes';
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ class AuthenticationService {
|
|||||||
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
return data['token'];
|
return data['token'];
|
||||||
} else if (response.statusCode == 400 &&
|
} else if (response.statusCode == 400 &&
|
||||||
response.body.toLowerCase().contains("no required certificate was sent")) {
|
response.body
|
||||||
|
.toLowerCase()
|
||||||
|
.contains("no required certificate was sent")) {
|
||||||
throw const ErrorMessage(ErrorCode.invalidClientCertificateConfiguration);
|
throw const ErrorMessage(ErrorCode.invalidClientCertificateConfiguration);
|
||||||
} else {
|
} else {
|
||||||
throw const ErrorMessage(ErrorCode.authenticationFailed);
|
throw const ErrorMessage(ErrorCode.authenticationFailed);
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_mobile/core/model/error_message.dart';
|
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({Key? key}) : super(key: key);
|
const LoginPage({Key? key}) : super(key: key);
|
||||||
@@ -73,7 +71,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
Widget _buildLoginButton() {
|
Widget _buildLoginButton() {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: MaterialStatePropertyAll(Theme.of(context).colorScheme.primaryContainer),
|
backgroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.primaryContainer),
|
||||||
elevation: const MaterialStatePropertyAll(0),
|
elevation: const MaterialStatePropertyAll(0),
|
||||||
),
|
),
|
||||||
onPressed: _login,
|
onPressed: _login,
|
||||||
@@ -83,19 +82,17 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _login() async {
|
void _login() {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
setState(() => _isLoginLoading = true);
|
setState(() => _isLoginLoading = true);
|
||||||
final form = _formKey.currentState?.value;
|
final form = _formKey.currentState?.value;
|
||||||
getIt<AuthenticationCubit>()
|
BlocProvider.of<AuthenticationCubit>(context)
|
||||||
.login(
|
.login(
|
||||||
credentials: form?[UserCredentialsFormField.fkCredentials],
|
credentials: form?[UserCredentialsFormField.fkCredentials],
|
||||||
serverUrl: form?[ServerAddressFormField.fkServerAddress],
|
serverUrl: form?[ServerAddressFormField.fkServerAddress],
|
||||||
clientCertificate: form?[ClientCertificateFormField.fkClientCertificate],
|
clientCertificate:
|
||||||
) //TODO: Move Intro slider route push here!
|
form?[ClientCertificateFormField.fkClientCertificate],
|
||||||
.onError<ErrorMessage>(
|
|
||||||
(error, _) => showError(context, error),
|
|
||||||
)
|
)
|
||||||
.whenComplete(() => setState(() => _isLoginLoading = false));
|
.whenComplete(() => setState(() => _isLoginLoading = false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ class ClientCertificateFormField extends StatefulWidget {
|
|||||||
const ClientCertificateFormField({Key? key}) : super(key: key);
|
const ClientCertificateFormField({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ClientCertificateFormField> createState() => _ClientCertificateFormFieldState();
|
State<ClientCertificateFormField> createState() =>
|
||||||
|
_ClientCertificateFormFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField> {
|
class _ClientCertificateFormFieldState
|
||||||
|
extends State<ClientCertificateFormField> {
|
||||||
File? _selectedFile;
|
File? _selectedFile;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -28,14 +30,17 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
}
|
}
|
||||||
assert(_selectedFile != null);
|
assert(_selectedFile != null);
|
||||||
if (_selectedFile?.path.split(".").last != 'pfx') {
|
if (_selectedFile?.path.split(".").last != 'pfx') {
|
||||||
return S.of(context).loginPageClientCertificateSettingInvalidFileFormatValidationText;
|
return S
|
||||||
|
.of(context)
|
||||||
|
.loginPageClientCertificateSettingInvalidFileFormatValidationText;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
title: Text(S.of(context).loginPageClientCertificateSettingLabel),
|
title: Text(S.of(context).loginPageClientCertificateSettingLabel),
|
||||||
subtitle: Text(S.of(context).loginPageClientCertificateSettingDescriptionText),
|
subtitle: Text(
|
||||||
|
S.of(context).loginPageClientCertificateSettingDescriptionText),
|
||||||
children: [
|
children: [
|
||||||
InputDecorator(
|
InputDecorator(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -69,7 +74,9 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
onChanged: (value) => field.didChange(
|
onChanged: (value) => field.didChange(
|
||||||
field.value?.copyWith(passphrase: value),
|
field.value?.copyWith(passphrase: value),
|
||||||
),
|
),
|
||||||
label: S.of(context).loginPageClientCertificatePassphraseLabel,
|
label: S
|
||||||
|
.of(context)
|
||||||
|
.loginPageClientCertificatePassphraseLabel,
|
||||||
).padded(),
|
).padded(),
|
||||||
] else
|
] else
|
||||||
...[]
|
...[]
|
||||||
@@ -90,8 +97,9 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_selectedFile = file;
|
_selectedFile = file;
|
||||||
});
|
});
|
||||||
final changedValue = field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
final changedValue =
|
||||||
ClientCertificate(bytes: file.readAsBytesSync());
|
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
||||||
|
ClientCertificate(bytes: file.readAsBytesSync());
|
||||||
field.didChange(changedValue);
|
field.didChange(changedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ class ObscuredInputTextFormField extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ObscuredInputTextFormField> createState() => _ObscuredInputTextFormFieldState();
|
State<ObscuredInputTextFormField> createState() =>
|
||||||
|
_ObscuredInputTextFormFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ObscuredInputTextFormFieldState extends State<ObscuredInputTextFormField> {
|
class _ObscuredInputTextFormFieldState
|
||||||
|
extends State<ObscuredInputTextFormField> {
|
||||||
bool _showPassword = false;
|
bool _showPassword = false;
|
||||||
final FocusNode _passwordFocusNode = FocusNode();
|
final FocusNode _passwordFocusNode = FocusNode();
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
|||||||
}
|
}
|
||||||
//https://stackoverflow.com/questions/49648022/check-whether-there-is-an-internet-connection-available-on-flutter-app
|
//https://stackoverflow.com/questions/49648022/check-whether-there-is-an-internet-connection-available-on-flutter-app
|
||||||
setState(() => _reachabilityStatus = ReachabilityStatus.testing);
|
setState(() => _reachabilityStatus = ReachabilityStatus.testing);
|
||||||
final isReachable = await getIt<ConnectivityStatusService>().isServerReachable(address);
|
final isReachable =
|
||||||
|
await getIt<ConnectivityStatusService>().isServerReachable(address);
|
||||||
if (isReachable) {
|
if (isReachable) {
|
||||||
setState(() => _reachabilityStatus = ReachabilityStatus.reachable);
|
setState(() => _reachabilityStatus = ReachabilityStatus.reachable);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
|
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/password_text_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/password_text_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
|
||||||
|
|
||||||
class UserCredentialsFormField extends StatefulWidget {
|
class UserCredentialsFormField extends StatefulWidget {
|
||||||
static const fkCredentials = 'credentials';
|
static const fkCredentials = 'credentials';
|
||||||
const UserCredentialsFormField({Key? key}) : super(key: key);
|
const UserCredentialsFormField({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<UserCredentialsFormField> createState() => _UserCredentialsFormFieldState();
|
State<UserCredentialsFormField> createState() =>
|
||||||
|
_UserCredentialsFormFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
||||||
@@ -28,7 +29,8 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
// USERNAME
|
// USERNAME
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
onChanged: (username) => field.didChange(
|
onChanged: (username) => field.didChange(
|
||||||
field.value?.copyWith(username: username) ?? UserCredentials(username: username),
|
field.value?.copyWith(username: username) ??
|
||||||
|
UserCredentials(username: username),
|
||||||
),
|
),
|
||||||
validator: FormBuilderValidators.required(
|
validator: FormBuilderValidators.required(
|
||||||
errorText: S.of(context).loginPageUsernameValidatorMessageText,
|
errorText: S.of(context).loginPageUsernameValidatorMessageText,
|
||||||
@@ -41,7 +43,8 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
ObscuredInputTextFormField(
|
ObscuredInputTextFormField(
|
||||||
label: S.of(context).loginPagePasswordFieldLabel,
|
label: S.of(context).loginPagePasswordFieldLabel,
|
||||||
onChanged: (password) => field.didChange(
|
onChanged: (password) => field.didChange(
|
||||||
field.value?.copyWith(password: password) ?? UserCredentials(password: password),
|
field.value?.copyWith(password: password) ??
|
||||||
|
UserCredentials(password: password),
|
||||||
),
|
),
|
||||||
validator: FormBuilderValidators.required(
|
validator: FormBuilderValidators.required(
|
||||||
errorText: S.of(context).loginPagePasswordValidatorMessageText,
|
errorText: S.of(context).loginPagePasswordValidatorMessageText,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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:paperless_mobile/core/model/error_message.dart';
|
import 'package:paperless_mobile/core/model/error_message.dart';
|
||||||
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/di_initializer.dart';
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
||||||
@@ -44,7 +45,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
|
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
|
||||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||||
|
|
||||||
Map<String, String> _errors = {};
|
PaperlessValidationErrors _errors = {};
|
||||||
bool _isUploadLoading = false;
|
bool _isUploadLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,7 +62,8 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
title: Text(S.of(context).documentsUploadPageTitle),
|
title: Text(S.of(context).documentsUploadPageTitle),
|
||||||
bottom: _isUploadLoading
|
bottom: _isUploadLoading
|
||||||
? const PreferredSize(
|
? const PreferredSize(
|
||||||
child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0))
|
child: LinearProgressIndicator(),
|
||||||
|
preferredSize: Size.fromHeight(4.0))
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
@@ -86,14 +88,17 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_formKey.currentState?.fields[DocumentModel.titleKey]?.didChange("");
|
_formKey.currentState?.fields[DocumentModel.titleKey]
|
||||||
_formKey.currentState?.fields[fkFileName]?.didChange(".pdf");
|
?.didChange("");
|
||||||
|
_formKey.currentState?.fields[fkFileName]
|
||||||
|
?.didChange(".pdf");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
errorText: _errors[DocumentModel.titleKey],
|
errorText: _errors[DocumentModel.titleKey],
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final String? transformedValue = value?.replaceAll(RegExp(r"[\W_]"), "_");
|
final String? transformedValue =
|
||||||
|
value?.replaceAll(RegExp(r"[\W_]"), "_");
|
||||||
_formKey.currentState?.fields[fkFileName]
|
_formKey.currentState?.fields[fkFileName]
|
||||||
?.didChange("${transformedValue ?? ''}.pdf");
|
?.didChange("${transformedValue ?? ''}.pdf");
|
||||||
},
|
},
|
||||||
@@ -106,7 +111,8 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: S.of(context).documentUploadFileNameLabel,
|
labelText: S.of(context).documentUploadFileNameLabel,
|
||||||
),
|
),
|
||||||
initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}.pdf",
|
initialValue:
|
||||||
|
"scan_${fileNameDateFormat.format(DateTime.now())}.pdf",
|
||||||
),
|
),
|
||||||
FormBuilderDateTimePicker(
|
FormBuilderDateTimePicker(
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
@@ -125,7 +131,8 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
return LabelFormField<DocumentType, DocumentTypeQuery>(
|
return LabelFormField<DocumentType, DocumentTypeQuery>(
|
||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
|
labelCreationWidgetBuilder: (initialValue) =>
|
||||||
|
BlocProvider.value(
|
||||||
value: BlocProvider.of<DocumentTypeCubit>(context),
|
value: BlocProvider.of<DocumentTypeCubit>(context),
|
||||||
child: AddDocumentTypePage(initialName: initialValue),
|
child: AddDocumentTypePage(initialName: initialValue),
|
||||||
),
|
),
|
||||||
@@ -133,7 +140,8 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
name: DocumentModel.documentTypeKey,
|
name: DocumentModel.documentTypeKey,
|
||||||
state: state,
|
state: state,
|
||||||
queryParameterIdBuilder: DocumentTypeQuery.fromId,
|
queryParameterIdBuilder: DocumentTypeQuery.fromId,
|
||||||
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
|
queryParameterNotAssignedBuilder:
|
||||||
|
DocumentTypeQuery.notAssigned,
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -144,15 +152,18 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
return LabelFormField<Correspondent, CorrespondentQuery>(
|
return LabelFormField<Correspondent, CorrespondentQuery>(
|
||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
|
labelCreationWidgetBuilder: (initialValue) =>
|
||||||
|
BlocProvider.value(
|
||||||
value: BlocProvider.of<CorrespondentCubit>(context),
|
value: BlocProvider.of<CorrespondentCubit>(context),
|
||||||
child: AddCorrespondentPage(initalValue: initialValue),
|
child: AddCorrespondentPage(initalValue: initialValue),
|
||||||
),
|
),
|
||||||
label: S.of(context).documentCorrespondentPropertyLabel + " *",
|
label:
|
||||||
|
S.of(context).documentCorrespondentPropertyLabel + " *",
|
||||||
name: DocumentModel.correspondentKey,
|
name: DocumentModel.correspondentKey,
|
||||||
state: state,
|
state: state,
|
||||||
queryParameterIdBuilder: CorrespondentQuery.fromId,
|
queryParameterIdBuilder: CorrespondentQuery.fromId,
|
||||||
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
|
queryParameterNotAssignedBuilder:
|
||||||
|
CorrespondentQuery.notAssigned,
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -188,19 +199,27 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
getIt<DocumentsCubit>().reloadDocuments();
|
getIt<DocumentsCubit>().reloadDocuments();
|
||||||
},
|
},
|
||||||
label: S.of(context).documentUploadProcessingSuccessfulReloadActionText,
|
label: S
|
||||||
|
.of(context)
|
||||||
|
.documentUploadProcessingSuccessfulReloadActionText,
|
||||||
),
|
),
|
||||||
content: Text(S.of(context).documentUploadProcessingSuccessfulText),
|
content:
|
||||||
|
Text(S.of(context).documentUploadProcessingSuccessfulText),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
title: _formKey.currentState?.value[DocumentModel.titleKey],
|
title: _formKey.currentState?.value[DocumentModel.titleKey],
|
||||||
documentType:
|
documentType: (_formKey.currentState
|
||||||
(_formKey.currentState?.value[DocumentModel.documentTypeKey] as IdQueryParameter).id,
|
?.value[DocumentModel.documentTypeKey] as IdQueryParameter)
|
||||||
correspondent:
|
.id,
|
||||||
(_formKey.currentState?.value[DocumentModel.correspondentKey] as IdQueryParameter).id,
|
correspondent: (_formKey.currentState
|
||||||
tags: (_formKey.currentState?.value[DocumentModel.tagsKey] as TagsQuery).ids,
|
?.value[DocumentModel.correspondentKey] as IdQueryParameter)
|
||||||
createdAt: (_formKey.currentState?.value[DocumentModel.createdKey] as DateTime?),
|
.id,
|
||||||
|
tags:
|
||||||
|
(_formKey.currentState?.value[DocumentModel.tagsKey] as TagsQuery)
|
||||||
|
.ids,
|
||||||
|
createdAt: (_formKey.currentState?.value[DocumentModel.createdKey]
|
||||||
|
as DateTime?),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isUploadLoading = false;
|
_isUploadLoading = false;
|
||||||
@@ -210,7 +229,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
|||||||
showSnackBar(context, S.of(context).documentUploadSuccessText);
|
showSnackBar(context, S.of(context).documentUploadSuccessText);
|
||||||
} on ErrorMessage catch (error) {
|
} on ErrorMessage catch (error) {
|
||||||
showError(context, error);
|
showError(context, error);
|
||||||
} on Map<String, String> catch (errorMessages) {
|
} on PaperlessValidationErrors catch (errorMessages) {
|
||||||
setState(() => _errors = errorMessages);
|
setState(() => _errors = errorMessages);
|
||||||
} catch (other) {
|
} catch (other) {
|
||||||
showSnackBar(context, other.toString());
|
showSnackBar(context, other.toString());
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ class ScannerPage extends StatefulWidget {
|
|||||||
State<ScannerPage> createState() => _ScannerPageState();
|
State<ScannerPage> createState() => _ScannerPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStateMixin {
|
class _ScannerPageState extends State<ScannerPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late final AnimationController _fabPulsingController;
|
late final AnimationController _fabPulsingController;
|
||||||
late final Animation _animation;
|
late final Animation _animation;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_fabPulsingController = AnimationController(vsync: this, duration: const Duration(seconds: 1))
|
_fabPulsingController =
|
||||||
..repeat(reverse: true);
|
AnimationController(vsync: this, duration: const Duration(seconds: 1))
|
||||||
|
..repeat(reverse: true);
|
||||||
_animation = Tween(begin: 1.0, end: 1.2).animate(_fabPulsingController)
|
_animation = Tween(begin: 1.0, end: 1.2).animate(_fabPulsingController)
|
||||||
..addListener(() {
|
..addListener(() {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -113,7 +115,8 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
|
|||||||
final img = pw.MemoryImage(element.readAsBytesSync());
|
final img = pw.MemoryImage(element.readAsBytesSync());
|
||||||
doc.addPage(
|
doc.addPage(
|
||||||
pw.Page(
|
pw.Page(
|
||||||
pageFormat: PdfPageFormat(img.width!.toDouble(), img.height!.toDouble()),
|
pageFormat:
|
||||||
|
PdfPageFormat(img.width!.toDouble(), img.height!.toDouble()),
|
||||||
build: (context) => pw.Image(img),
|
build: (context) => pw.Image(img),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -164,7 +167,8 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return GridImageItemWidget(
|
return GridImageItemWidget(
|
||||||
file: scans[index],
|
file: scans[index],
|
||||||
onDelete: () => BlocProvider.of<DocumentScannerCubit>(context).removeScan(index),
|
onDelete: () => BlocProvider.of<DocumentScannerCubit>(context)
|
||||||
|
.removeScan(index),
|
||||||
index: index,
|
index: index,
|
||||||
totalNumberOfFiles: scans.length,
|
totalNumberOfFiles: scans.length,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ class _ScannerWidgetState extends State<ScannerWidget> {
|
|||||||
appBar: AppBar(title: const Text("Scan document")),
|
appBar: AppBar(title: const Text("Scan document")),
|
||||||
body: FutureBuilder<PermissionStatus>(
|
body: FutureBuilder<PermissionStatus>(
|
||||||
future: Permission.camera.request(),
|
future: Permission.camera.request(),
|
||||||
builder: (BuildContext context, AsyncSnapshot<PermissionStatus> snapshot) {
|
builder:
|
||||||
|
(BuildContext context, AsyncSnapshot<PermissionStatus> snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user