mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 12:08:05 -06:00
feat: Rework error handling, upgrade dio, fixed bugs
- Fix grey screen bug when adding labels from documnet upload - Add more permission checks to conditionally show widgets
This commit is contained in:
@@ -1,99 +1,46 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
|
||||
class DioHttpErrorInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(DioError err, ErrorInterceptorHandler handler) {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (err.response?.statusCode == 400) {
|
||||
// try to parse contained error message, otherwise return response
|
||||
final dynamic data = err.response?.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
return _handlePaperlessValidationError(data, handler, err);
|
||||
} else if (data is String) {
|
||||
return _handlePlainError(data, handler, err);
|
||||
}
|
||||
} else if (err.response?.statusCode == 403) {
|
||||
var data = err.response!.data;
|
||||
if (data is Map && data.containsKey("detail")) {
|
||||
final data = err.response!.data;
|
||||
if (PaperlessServerMessageException.canParse(data)) {
|
||||
final exception = PaperlessServerMessageException.fromJson(data);
|
||||
final message = exception.detail;
|
||||
handler.reject(
|
||||
DioError(
|
||||
message: data['detail'],
|
||||
DioException(
|
||||
message: message,
|
||||
requestOptions: err.requestOptions,
|
||||
error: ServerMessageException(data['detail']),
|
||||
error: exception,
|
||||
response: err.response,
|
||||
type: DioErrorType.unknown,
|
||||
type: DioExceptionType.badResponse,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (err.error is SocketException) {
|
||||
final ex = err.error as SocketException;
|
||||
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
|
||||
return handler.reject(
|
||||
DioError(
|
||||
message: "The server could not be reached. Is the device offline?",
|
||||
error: const PaperlessServerException(ErrorCode.deviceOffline),
|
||||
} else if (PaperlessFormValidationException.canParse(data)) {
|
||||
final exception = PaperlessFormValidationException.fromJson(data);
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioErrorType.connectionTimeout,
|
||||
error: exception,
|
||||
response: err.response,
|
||||
type: DioExceptionType.badResponse,
|
||||
),
|
||||
);
|
||||
} else if (data is String &&
|
||||
data.contains("No required SSL certificate was sent")) {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioExceptionType.badResponse,
|
||||
error:
|
||||
const PaperlessApiException(ErrorCode.missingClientCertificate),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return handler.reject(err);
|
||||
}
|
||||
|
||||
void _handlePaperlessValidationError(
|
||||
Map<String, dynamic> json,
|
||||
ErrorInterceptorHandler handler,
|
||||
DioError err,
|
||||
) {
|
||||
final PaperlessValidationErrors errorMessages = {};
|
||||
for (final entry in json.entries) {
|
||||
if (entry.value is List) {
|
||||
errorMessages.putIfAbsent(
|
||||
entry.key,
|
||||
() => (entry.value as List).cast<String>().first,
|
||||
);
|
||||
} else if (entry.value is String) {
|
||||
errorMessages.putIfAbsent(entry.key, () => entry.value);
|
||||
} else {
|
||||
errorMessages.putIfAbsent(entry.key, () => entry.value.toString());
|
||||
}
|
||||
}
|
||||
handler.reject(
|
||||
DioError(
|
||||
error: errorMessages,
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioErrorType.badResponse,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handlePlainError(
|
||||
String data,
|
||||
ErrorInterceptorHandler handler,
|
||||
DioError err,
|
||||
) {
|
||||
if (data.contains("No required SSL certificate was sent")) {
|
||||
handler.reject(
|
||||
DioError(
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioErrorType.badResponse,
|
||||
error: const PaperlessServerException(
|
||||
ErrorCode.missingClientCertificate),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _OsErrorCodes {
|
||||
serverUnreachable(101);
|
||||
|
||||
const _OsErrorCodes(this.code);
|
||||
final int code;
|
||||
}
|
||||
|
||||
32
lib/core/interceptor/dio_offline_interceptor.dart
Normal file
32
lib/core/interceptor/dio_offline_interceptor.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
class DioOfflineInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (err.error is SocketException) {
|
||||
final ex = err.error as SocketException;
|
||||
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
|
||||
handler.reject(
|
||||
DioException(
|
||||
message: "The host could not be reached. Is your device offline?",
|
||||
error: const PaperlessApiException(ErrorCode.deviceOffline),
|
||||
requestOptions: err.requestOptions,
|
||||
type: DioExceptionType.connectionTimeout,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _OsErrorCodes {
|
||||
serverUnreachable(101);
|
||||
|
||||
const _OsErrorCodes(this.code);
|
||||
final int code;
|
||||
}
|
||||
27
lib/core/interceptor/dio_unauthorized_interceptor.dart
Normal file
27
lib/core/interceptor/dio_unauthorized_interceptor.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
class DioUnauthorizedInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (err.response?.statusCode == 403) {
|
||||
final data = err.response!.data;
|
||||
String? message;
|
||||
if (PaperlessServerMessageException.canParse(data)) {
|
||||
final exception = PaperlessServerMessageException.fromJson(data);
|
||||
message = exception.detail;
|
||||
}
|
||||
handler.reject(
|
||||
DioException(
|
||||
message: message,
|
||||
requestOptions: err.requestOptions,
|
||||
error: PaperlessUnauthorizedException(message),
|
||||
response: err.response,
|
||||
type: DioExceptionType.badResponse,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class RetryOnConnectionChangeInterceptor extends Interceptor {
|
||||
});
|
||||
|
||||
@override
|
||||
void onError(DioError err, ErrorInterceptorHandler handler) async {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
if (_shouldRetryOnHttpException(err)) {
|
||||
try {
|
||||
handler.resolve(await DioHttpRequestRetrier(dio: dio)
|
||||
@@ -27,8 +27,8 @@ class RetryOnConnectionChangeInterceptor extends Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldRetryOnHttpException(DioError err) {
|
||||
return err.type == DioErrorType.unknown &&
|
||||
bool _shouldRetryOnHttpException(DioException err) {
|
||||
return err.type == DioExceptionType.unknown &&
|
||||
(err.error is HttpException &&
|
||||
(err.message?.contains(
|
||||
'Connection closed before full header was received',
|
||||
|
||||
@@ -8,7 +8,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor {
|
||||
static const _missingClientCertText = "No required SSL certificate was sent";
|
||||
|
||||
@override
|
||||
void onError(DioError err, ErrorInterceptorHandler handler) {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (err.response?.statusCode == 400) {
|
||||
final message = err.response?.data;
|
||||
if (message is String && message.contains(_missingClientCertText)) {
|
||||
@@ -19,7 +19,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (err.type == DioErrorType.connectionTimeout) {
|
||||
if (err.type == DioExceptionType.connectionTimeout) {
|
||||
return _rejectWithStatus(
|
||||
ReachabilityStatus.connectionTimeout,
|
||||
err,
|
||||
@@ -48,13 +48,13 @@ class ServerReachabilityErrorInterceptor extends Interceptor {
|
||||
|
||||
void _rejectWithStatus(
|
||||
ReachabilityStatus reachabilityStatus,
|
||||
DioError err,
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
handler.reject(DioError(
|
||||
handler.reject(DioException(
|
||||
error: reachabilityStatus,
|
||||
requestOptions: err.requestOptions,
|
||||
response: err.response,
|
||||
type: DioErrorType.unknown,
|
||||
type: DioExceptionType.unknown,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -354,6 +354,7 @@ Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
|
||||
final labelRepo = context.read<LabelRepository>();
|
||||
final docsApi = context.read<PaperlessDocumentsApi>();
|
||||
final connectivity = context.read<Connectivity>();
|
||||
final apiVersion = context.read<ApiVersion>();
|
||||
return Navigator.of(context).push<DocumentUploadResult>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
@@ -361,6 +362,7 @@ Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
|
||||
Provider.value(value: labelRepo),
|
||||
Provider.value(value: docsApi),
|
||||
Provider.value(value: connectivity),
|
||||
Provider.value(value: apiVersion)
|
||||
],
|
||||
builder: (_, child) => BlocProvider(
|
||||
create: (_) => DocumentUploadCubit(
|
||||
|
||||
@@ -88,7 +88,6 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
|
||||
if (correspondent != null) {
|
||||
final updatedState = {...state.correspondents}..[id] = correspondent;
|
||||
emit(state.copyWith(correspondents: updatedState));
|
||||
|
||||
return correspondent;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -4,13 +4,14 @@ import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
|
||||
/// Manages the security context, authentication and base request URL for
|
||||
/// an underlying [Dio] client which is injected into all services
|
||||
/// requiring authenticated access to the Paperless HTTP API.
|
||||
/// requiring authenticated access to the Paperless REST API.
|
||||
class SessionManager extends ValueNotifier<Dio> {
|
||||
Dio get client => value;
|
||||
|
||||
@@ -20,16 +21,21 @@ class SessionManager extends ValueNotifier<Dio> {
|
||||
static Dio _initDio(List<Interceptor> interceptors) {
|
||||
//en- and decoded by utf8 by default
|
||||
final Dio dio = Dio(
|
||||
BaseOptions(contentType: Headers.jsonContentType),
|
||||
BaseOptions(
|
||||
contentType: Headers.jsonContentType,
|
||||
followRedirects: true,
|
||||
maxRedirects: 10,
|
||||
),
|
||||
);
|
||||
dio.options
|
||||
..receiveTimeout = const Duration(seconds: 30)
|
||||
..sendTimeout = const Duration(seconds: 60)
|
||||
..responseType = ResponseType.json;
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
|
||||
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient =
|
||||
() => HttpClient()..badCertificateCallback = (cert, host, port) => true;
|
||||
dio.interceptors.addAll([
|
||||
...interceptors,
|
||||
DioUnauthorizedInterceptor(),
|
||||
DioHttpErrorInterceptor(),
|
||||
PrettyDioLogger(
|
||||
compact: true,
|
||||
@@ -64,7 +70,7 @@ class SessionManager extends ValueNotifier<Dio> {
|
||||
password: clientCertificate.passphrase,
|
||||
);
|
||||
final adapter = IOHttpClientAdapter()
|
||||
..onHttpClientCreate = (client) => HttpClient(context: context)
|
||||
..createHttpClient = () => HttpClient(context: context)
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
return ReachabilityStatus.reachable;
|
||||
}
|
||||
return ReachabilityStatus.notReachable;
|
||||
} on DioError catch (error) {
|
||||
if (error.type == DioErrorType.unknown &&
|
||||
} on DioException catch (error) {
|
||||
if (error.type == DioExceptionType.unknown &&
|
||||
error.error is ReachabilityStatus) {
|
||||
return error.error as ReachabilityStatus;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class FileService {
|
||||
) async {
|
||||
final dir = await documentsDirectory;
|
||||
if (dir == null) {
|
||||
throw const PaperlessServerException.unknown(); //TODO: better handling
|
||||
throw const PaperlessApiException.unknown(); //TODO: better handling
|
||||
}
|
||||
File file = File("${dir.path}/$filename");
|
||||
return file..writeAsBytes(bytes);
|
||||
|
||||
@@ -3,74 +3,75 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
String translateError(BuildContext context, ErrorCode code) {
|
||||
switch (code) {
|
||||
case ErrorCode.unknown:
|
||||
return S.of(context)!.anUnknownErrorOccurred;
|
||||
case ErrorCode.authenticationFailed:
|
||||
return S.of(context)!.authenticationFailedPleaseTryAgain;
|
||||
case ErrorCode.notAuthenticated:
|
||||
return S.of(context)!.userIsNotAuthenticated;
|
||||
case ErrorCode.documentUploadFailed:
|
||||
return S.of(context)!.couldNotUploadDocument;
|
||||
case ErrorCode.documentUpdateFailed:
|
||||
return S.of(context)!.couldNotUpdateDocument;
|
||||
case ErrorCode.documentLoadFailed:
|
||||
return S.of(context)!.couldNotLoadDocuments;
|
||||
case ErrorCode.documentDeleteFailed:
|
||||
return S.of(context)!.couldNotDeleteDocument;
|
||||
case ErrorCode.documentPreviewFailed:
|
||||
return S.of(context)!.couldNotLoadDocumentPreview;
|
||||
case ErrorCode.documentAsnQueryFailed:
|
||||
return S.of(context)!.couldNotAssignArchiveSerialNumber;
|
||||
case ErrorCode.tagCreateFailed:
|
||||
return S.of(context)!.couldNotCreateTag;
|
||||
case ErrorCode.tagLoadFailed:
|
||||
return S.of(context)!.couldNotLoadTags;
|
||||
case ErrorCode.documentTypeCreateFailed:
|
||||
return S.of(context)!.couldNotCreateDocument;
|
||||
case ErrorCode.documentTypeLoadFailed:
|
||||
return S.of(context)!.couldNotLoadDocumentTypes;
|
||||
case ErrorCode.correspondentCreateFailed:
|
||||
return S.of(context)!.couldNotCreateCorrespondent;
|
||||
case ErrorCode.correspondentLoadFailed:
|
||||
return S.of(context)!.couldNotLoadCorrespondents;
|
||||
case ErrorCode.scanRemoveFailed:
|
||||
return S.of(context)!.anErrorOccurredRemovingTheScans;
|
||||
case ErrorCode.invalidClientCertificateConfiguration:
|
||||
return S.of(context)!.invalidCertificateOrMissingPassphrase;
|
||||
case ErrorCode.documentBulkActionFailed:
|
||||
return S.of(context)!.couldNotBulkEditDocuments;
|
||||
case ErrorCode.biometricsNotSupported:
|
||||
return S.of(context)!.biometricAuthenticationNotSupported;
|
||||
case ErrorCode.biometricAuthenticationFailed:
|
||||
return S.of(context)!.biometricAuthenticationFailed;
|
||||
case ErrorCode.deviceOffline:
|
||||
return S.of(context)!.youAreCurrentlyOffline;
|
||||
case ErrorCode.serverUnreachable:
|
||||
return S.of(context)!.couldNotReachYourPaperlessServer;
|
||||
case ErrorCode.similarQueryError:
|
||||
return S.of(context)!.couldNotLoadSimilarDocuments;
|
||||
case ErrorCode.autocompleteQueryError:
|
||||
return S.of(context)!.anErrorOccurredWhileTryingToAutocompleteYourQuery;
|
||||
case ErrorCode.storagePathLoadFailed:
|
||||
return S.of(context)!.couldNotLoadStoragePaths;
|
||||
case ErrorCode.storagePathCreateFailed:
|
||||
return S.of(context)!.couldNotCreateStoragePath;
|
||||
case ErrorCode.loadSavedViewsError:
|
||||
return S.of(context)!.couldNotLoadSavedViews;
|
||||
case ErrorCode.createSavedViewError:
|
||||
return S.of(context)!.couldNotCreateSavedView;
|
||||
case ErrorCode.deleteSavedViewError:
|
||||
return S.of(context)!.couldNotDeleteSavedView;
|
||||
case ErrorCode.requestTimedOut:
|
||||
return S.of(context)!.requestTimedOut;
|
||||
case ErrorCode.unsupportedFileFormat:
|
||||
return S.of(context)!.fileFormatNotSupported;
|
||||
case ErrorCode.missingClientCertificate:
|
||||
return S.of(context)!.aClientCertificateWasExpectedButNotSent;
|
||||
case ErrorCode.suggestionsQueryError:
|
||||
return S.of(context)!.couldNotLoadSuggestions;
|
||||
case ErrorCode.acknowledgeTasksError:
|
||||
return S.of(context)!.couldNotAcknowledgeTasks;
|
||||
}
|
||||
return switch (code) {
|
||||
ErrorCode.unknown => S.of(context)!.anUnknownErrorOccurred,
|
||||
ErrorCode.authenticationFailed =>
|
||||
S.of(context)!.authenticationFailedPleaseTryAgain,
|
||||
ErrorCode.notAuthenticated => S.of(context)!.userIsNotAuthenticated,
|
||||
ErrorCode.documentUploadFailed => S.of(context)!.couldNotUploadDocument,
|
||||
ErrorCode.documentUpdateFailed => S.of(context)!.couldNotUpdateDocument,
|
||||
ErrorCode.documentLoadFailed => S.of(context)!.couldNotLoadDocuments,
|
||||
ErrorCode.documentDeleteFailed => S.of(context)!.couldNotDeleteDocument,
|
||||
ErrorCode.documentPreviewFailed =>
|
||||
S.of(context)!.couldNotLoadDocumentPreview,
|
||||
ErrorCode.documentAsnQueryFailed =>
|
||||
S.of(context)!.couldNotAssignArchiveSerialNumber,
|
||||
ErrorCode.tagCreateFailed => S.of(context)!.couldNotCreateTag,
|
||||
ErrorCode.tagLoadFailed => S.of(context)!.couldNotLoadTags,
|
||||
ErrorCode.documentTypeCreateFailed => S.of(context)!.couldNotCreateDocument,
|
||||
ErrorCode.documentTypeLoadFailed =>
|
||||
S.of(context)!.couldNotLoadDocumentTypes,
|
||||
ErrorCode.correspondentCreateFailed =>
|
||||
S.of(context)!.couldNotCreateCorrespondent,
|
||||
ErrorCode.correspondentLoadFailed =>
|
||||
S.of(context)!.couldNotLoadCorrespondents,
|
||||
ErrorCode.scanRemoveFailed =>
|
||||
S.of(context)!.anErrorOccurredRemovingTheScans,
|
||||
ErrorCode.invalidClientCertificateConfiguration =>
|
||||
S.of(context)!.invalidCertificateOrMissingPassphrase,
|
||||
ErrorCode.documentBulkActionFailed =>
|
||||
S.of(context)!.couldNotBulkEditDocuments,
|
||||
ErrorCode.biometricsNotSupported =>
|
||||
S.of(context)!.biometricAuthenticationNotSupported,
|
||||
ErrorCode.biometricAuthenticationFailed =>
|
||||
S.of(context)!.biometricAuthenticationFailed,
|
||||
ErrorCode.deviceOffline => S.of(context)!.youAreCurrentlyOffline,
|
||||
ErrorCode.serverUnreachable =>
|
||||
S.of(context)!.couldNotReachYourPaperlessServer,
|
||||
ErrorCode.similarQueryError => S.of(context)!.couldNotLoadSimilarDocuments,
|
||||
ErrorCode.autocompleteQueryError =>
|
||||
S.of(context)!.anErrorOccurredWhileTryingToAutocompleteYourQuery,
|
||||
ErrorCode.storagePathLoadFailed => S.of(context)!.couldNotLoadStoragePaths,
|
||||
ErrorCode.storagePathCreateFailed =>
|
||||
S.of(context)!.couldNotCreateStoragePath,
|
||||
ErrorCode.loadSavedViewsError => S.of(context)!.couldNotLoadSavedViews,
|
||||
ErrorCode.createSavedViewError => S.of(context)!.couldNotCreateSavedView,
|
||||
ErrorCode.deleteSavedViewError => S.of(context)!.couldNotDeleteSavedView,
|
||||
ErrorCode.requestTimedOut => S.of(context)!.requestTimedOut,
|
||||
ErrorCode.unsupportedFileFormat => S.of(context)!.fileFormatNotSupported,
|
||||
ErrorCode.missingClientCertificate =>
|
||||
S.of(context)!.aClientCertificateWasExpectedButNotSent,
|
||||
ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions,
|
||||
ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks,
|
||||
ErrorCode.correspondentDeleteFailed =>
|
||||
"Could not delete correspondent, please try again.",
|
||||
ErrorCode.documentTypeDeleteFailed =>
|
||||
"Could not delete document type, please try again.",
|
||||
ErrorCode.tagDeleteFailed => "Could not delete tag, please try again.",
|
||||
ErrorCode.correspondentUpdateFailed =>
|
||||
"Could not update correspondent, please try again.",
|
||||
ErrorCode.documentTypeUpdateFailed =>
|
||||
"Could not update document type, please try again.",
|
||||
ErrorCode.tagUpdateFailed => "Could not update tag, please try again.",
|
||||
ErrorCode.storagePathDeleteFailed =>
|
||||
"Could not delete storage path, please try again.",
|
||||
ErrorCode.storagePathUpdateFailed =>
|
||||
"Could not update storage path, please try again.",
|
||||
ErrorCode.serverInformationLoadFailed =>
|
||||
"Could not load server information.",
|
||||
ErrorCode.serverStatisticsLoadFailed => "Could not load server statistics.",
|
||||
ErrorCode.uiSettingsLoadFailed => "Could not load UI settings",
|
||||
ErrorCode.loadTasksError => "Could not load tasks.",
|
||||
ErrorCode.userNotFound => "User could not be found.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
typedef JSON = Map<String, dynamic>;
|
||||
typedef PaperlessValidationErrors = Map<String, String>;
|
||||
typedef PaperlessLocalizedErrorMessage = String;
|
||||
|
||||
extension ValidationErrorsUtils on PaperlessValidationErrors {
|
||||
bool get hasFieldUnspecificError => containsKey("non_field_errors");
|
||||
String? get fieldUnspecificError => this['non_field_errors'];
|
||||
}
|
||||
@@ -70,13 +70,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
|
||||
Future<void> loadFullContent() async {
|
||||
final doc = await _api.find(state.document.id);
|
||||
if (doc == null) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(
|
||||
isFullContentLoaded: true,
|
||||
fullContent: doc.content,
|
||||
));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isFullContentLoaded: true,
|
||||
fullContent: doc.content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> assignAsn(
|
||||
@@ -99,13 +98,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
|
||||
Future<ResultType> openDocumentInSystemViewer() async {
|
||||
final cacheDir = await FileService.temporaryDirectory;
|
||||
//TODO: Why is this cleared here?
|
||||
await FileService.clearDirectoryContent(PaperlessDirectoryType.temporary);
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
}
|
||||
final desc = FileDescription.fromPath(
|
||||
state.metaData!.mediaFilename.replaceAll("/", " "));
|
||||
state.metaData!.mediaFilename.replaceAll("/", " "),
|
||||
);
|
||||
|
||||
final fileName = "${desc.filename}.pdf";
|
||||
final file = File("${cacheDir.path}/$fileName");
|
||||
|
||||
@@ -287,8 +287,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
|
||||
Widget _buildEditButton() {
|
||||
bool canEdit = context.watchInternetConnection &&
|
||||
LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.change, PermissionTarget.document);
|
||||
LocalUserAccount.current.paperlessUser.canEditDocuments;
|
||||
if (!canEdit) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@@ -319,8 +318,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
final isConnected = connectivityState.isConnected;
|
||||
|
||||
final canDelete = isConnected &&
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete, PermissionTarget.document);
|
||||
LocalUserAccount.current.paperlessUser.canDeleteDocuments;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
@@ -430,7 +428,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
try {
|
||||
await context.read<DocumentDetailsCubit>().delete(document);
|
||||
showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} finally {
|
||||
// Document deleted => go back to primary route
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
@@ -48,10 +47,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userCanEditDocument =
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.document,
|
||||
);
|
||||
LocalUserAccount.current.paperlessUser.canEditDocuments;
|
||||
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.document.archiveSerialNumber !=
|
||||
@@ -124,12 +120,14 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
.read<DocumentDetailsCubit>()
|
||||
.assignAsn(widget.document, asn: asn)
|
||||
.then((value) => _onAsnUpdated())
|
||||
.onError<PaperlessServerException>(
|
||||
.onError<PaperlessApiException>(
|
||||
(error, stackTrace) => showErrorMessage(context, error, stackTrace),
|
||||
)
|
||||
.onError<PaperlessValidationErrors>(
|
||||
(error, stackTrace) => setState(() => _errors = error),
|
||||
);
|
||||
.onError<PaperlessFormValidationException>(
|
||||
(error, stackTrace) {
|
||||
setState(() => _errors = error.validationMessages);
|
||||
},
|
||||
);
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
@@ -141,9 +139,10 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
autoAssign: true,
|
||||
)
|
||||
.then((value) => _onAsnUpdated())
|
||||
.onError<PaperlessServerException>(
|
||||
.onError<PaperlessApiException>(
|
||||
(error, stackTrace) => showErrorMessage(context, error, stackTrace),
|
||||
);
|
||||
)
|
||||
.catchError((error) => showGenericError(context, error));
|
||||
}
|
||||
|
||||
void _onAsnUpdated() {
|
||||
|
||||
@@ -95,7 +95,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
||||
locale: globalSettings.preferredLocaleSubtag,
|
||||
);
|
||||
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} catch (error) {
|
||||
showGenericError(context, error);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
|
||||
@@ -45,38 +46,35 @@ class DocumentOverviewWidget extends StatelessWidget {
|
||||
context: context,
|
||||
label: S.of(context)!.createdAt,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
Visibility(
|
||||
visible: document.documentType != null,
|
||||
child: DetailsItem(
|
||||
if (document.documentType != null &&
|
||||
LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
|
||||
DetailsItem(
|
||||
label: S.of(context)!.documentType,
|
||||
content: LabelText<DocumentType>(
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
label: availableDocumentTypes[document.documentType],
|
||||
),
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
),
|
||||
Visibility(
|
||||
visible: document.correspondent != null,
|
||||
child: DetailsItem(
|
||||
if (document.correspondent != null &&
|
||||
LocalUserAccount.current.paperlessUser.canViewCorrespondents)
|
||||
DetailsItem(
|
||||
label: S.of(context)!.correspondent,
|
||||
content: LabelText<Correspondent>(
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
label: availableCorrespondents[document.correspondent],
|
||||
),
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
),
|
||||
Visibility(
|
||||
visible: document.storagePath != null,
|
||||
child: DetailsItem(
|
||||
if (document.storagePath != null &&
|
||||
LocalUserAccount.current.paperlessUser.canViewStoragePaths)
|
||||
DetailsItem(
|
||||
label: S.of(context)!.storagePath,
|
||||
content: LabelText<StoragePath>(
|
||||
label: availableStoragePaths[document.storagePath],
|
||||
),
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
),
|
||||
Visibility(
|
||||
visible: document.tags.isNotEmpty,
|
||||
child: DetailsItem(
|
||||
if (document.tags.isNotEmpty &&
|
||||
LocalUserAccount.current.paperlessUser.canViewTags)
|
||||
DetailsItem(
|
||||
label: S.of(context)!.tags,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
@@ -86,7 +84,6 @@ class DocumentOverviewWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
|
||||
await context.read<DocumentDetailsCubit>().shareDocument(
|
||||
shareOriginal: original,
|
||||
);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} catch (error) {
|
||||
showGenericError(context, error);
|
||||
|
||||
@@ -123,12 +123,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
name: fkCorrespondent,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.correspondent,
|
||||
),
|
||||
canCreateNewLabel: LocalUserAccount.current
|
||||
.paperlessUser.canCreateCorrespondents,
|
||||
),
|
||||
if (_filteredSuggestions
|
||||
?.hasSuggestedCorrespondents ??
|
||||
@@ -164,12 +160,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
initialName: currentInput,
|
||||
),
|
||||
),
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.documentType,
|
||||
),
|
||||
canCreateNewLabel: LocalUserAccount.current
|
||||
.paperlessUser.canCreateDocumentTypes,
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType,
|
||||
initialValue:
|
||||
@@ -214,12 +206,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
child: AddStoragePathPage(
|
||||
initalName: initialValue),
|
||||
),
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.storagePath,
|
||||
),
|
||||
canCreateNewLabel: LocalUserAccount.current
|
||||
.paperlessUser.canCreateStoragePaths,
|
||||
addLabelText: S.of(context)!.addStoragePath,
|
||||
labelText: S.of(context)!.storagePath,
|
||||
options: state.storagePaths,
|
||||
@@ -328,7 +316,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
try {
|
||||
await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
|
||||
showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} finally {
|
||||
setState(() {
|
||||
|
||||
@@ -22,7 +22,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
|
||||
scans.removeAt(fileIndex);
|
||||
emit(scans);
|
||||
} catch (_) {
|
||||
throw const PaperlessServerException(ErrorCode.scanRemoveFailed);
|
||||
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
|
||||
imageCache.clear();
|
||||
emit([]);
|
||||
} catch (_) {
|
||||
throw const PaperlessServerException(ErrorCode.scanRemoveFailed);
|
||||
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,9 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: const SliverSearchBar(),
|
||||
sliver: SliverSearchBar(
|
||||
titleText: S.of(context)!.scanner,
|
||||
),
|
||||
),
|
||||
SliverOverlapAbsorber(
|
||||
handle: actionsHandle,
|
||||
@@ -322,7 +324,7 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
onDelete: () async {
|
||||
try {
|
||||
context.read<DocumentScannerCubit>().removeScan(index);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
},
|
||||
@@ -339,7 +341,7 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
void _reset(BuildContext context) {
|
||||
try {
|
||||
context.read<DocumentScannerCubit>().reset();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -360,7 +362,7 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
)) {
|
||||
showErrorMessage(
|
||||
context,
|
||||
const PaperlessServerException(ErrorCode.unsupportedFileFormat),
|
||||
const PaperlessApiException(ErrorCode.unsupportedFileFormat),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart';
|
||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SliverSearchBar extends StatelessWidget {
|
||||
final bool floating;
|
||||
final bool pinned;
|
||||
final String titleText;
|
||||
const SliverSearchBar({
|
||||
super.key,
|
||||
this.floating = false,
|
||||
this.pinned = false,
|
||||
required this.titleText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
sliver: SliverPersistentHeader(
|
||||
floating: floating,
|
||||
pinned: pinned,
|
||||
delegate: CustomizableSliverPersistentHeaderDelegate(
|
||||
minExtent: kToolbarHeight,
|
||||
maxExtent: kToolbarHeight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: const DocumentSearchBar(),
|
||||
),
|
||||
if (LocalUserAccount.current.paperlessUser.canViewDocuments) {
|
||||
return SliverAppBar(
|
||||
toolbarHeight: kToolbarHeight,
|
||||
flexibleSpace: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: const DocumentSearchBar(),
|
||||
),
|
||||
),
|
||||
);
|
||||
automaticallyImplyLeading: false,
|
||||
);
|
||||
} else {
|
||||
return SliverAppBar(
|
||||
title: Text(titleText),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(6),
|
||||
icon: GlobalSettingsBuilder(
|
||||
builder: (context, settings) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||
.listenable(),
|
||||
builder: (context, box, _) {
|
||||
final account = box.get(settings.currentLoggedInUser!)!;
|
||||
return UserAvatar(account: account);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
final apiVersion = context.read<ApiVersion>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Provider.value(
|
||||
value: apiVersion,
|
||||
child: const ManageAccountsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,17 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
|
||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DocumentUploadResult {
|
||||
final bool success;
|
||||
@@ -56,7 +57,7 @@ class _DocumentUploadPreparationPageState
|
||||
|
||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||
|
||||
PaperlessValidationErrors _errors = {};
|
||||
Map<String, String> _errors = {};
|
||||
bool _isUploadLoading = false;
|
||||
late bool _syncTitleAndFilename;
|
||||
bool _showDatePickerDeleteIcon = false;
|
||||
@@ -197,54 +198,64 @@ class _DocumentUploadPreparationPageState
|
||||
),
|
||||
),
|
||||
// Correspondent
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddCorrespondentPage(initialName: initialName),
|
||||
if (LocalUserAccount
|
||||
.current.paperlessUser.canViewCorrespondents)
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) => MultiProvider(
|
||||
providers: [
|
||||
Provider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
),
|
||||
Provider.value(
|
||||
value: context.read<ApiVersion>(),
|
||||
)
|
||||
],
|
||||
child: AddCorrespondentPage(initialName: initialName),
|
||||
),
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent + " *",
|
||||
name: DocumentModel.correspondentKey,
|
||||
options: state.correspondents,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser.canCreateCorrespondents,
|
||||
),
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent + " *",
|
||||
name: DocumentModel.correspondentKey,
|
||||
options: state.correspondents,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.correspondent,
|
||||
),
|
||||
),
|
||||
// Document type
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddDocumentTypePage(initialName: initialName),
|
||||
if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) => MultiProvider(
|
||||
providers: [
|
||||
Provider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
),
|
||||
Provider.value(
|
||||
value: context.read<ApiVersion>(),
|
||||
)
|
||||
],
|
||||
child: AddDocumentTypePage(initialName: initialName),
|
||||
),
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType + " *",
|
||||
name: DocumentModel.documentTypeKey,
|
||||
options: state.documentTypes,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel: LocalUserAccount
|
||||
.current.paperlessUser.canCreateDocumentTypes,
|
||||
),
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType + " *",
|
||||
name: DocumentModel.documentTypeKey,
|
||||
options: state.documentTypes,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.documentType,
|
||||
if (LocalUserAccount.current.paperlessUser.canViewTags)
|
||||
TagsFormField(
|
||||
name: DocumentModel.tagsKey,
|
||||
allowCreation: true,
|
||||
allowExclude: false,
|
||||
allowOnlySelection: true,
|
||||
options: state.tags,
|
||||
),
|
||||
),
|
||||
TagsFormField(
|
||||
name: DocumentModel.tagsKey,
|
||||
allowCreation: true,
|
||||
allowExclude: false,
|
||||
allowOnlySelection: true,
|
||||
options: state.tags,
|
||||
),
|
||||
Text(
|
||||
"* " + S.of(context)!.uploadInferValuesHint,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
@@ -301,14 +312,14 @@ class _DocumentUploadPreparationPageState
|
||||
context,
|
||||
DocumentUploadResult(true, taskId),
|
||||
);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on PaperlessValidationErrors catch (errors) {
|
||||
setState(() => _errors = errors);
|
||||
} on PaperlessFormValidationException catch (exception) {
|
||||
setState(() => _errors = exception.validationMessages);
|
||||
} catch (unknownError, stackTrace) {
|
||||
debugPrint(unknownError.toString());
|
||||
showErrorMessage(
|
||||
context, const PaperlessServerException.unknown(), stackTrace);
|
||||
context, const PaperlessApiException.unknown(), stackTrace);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isUploadLoading = false;
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
|
||||
@@ -53,14 +54,16 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final showSavedViews =
|
||||
LocalUserAccount.current.paperlessUser.canViewSavedViews;
|
||||
_tabController = TabController(
|
||||
length: 2,
|
||||
length: showSavedViews ? 2 : 1,
|
||||
vsync: this,
|
||||
);
|
||||
Future.wait([
|
||||
context.read<DocumentsCubit>().reload(),
|
||||
context.read<SavedViewCubit>().reload(),
|
||||
]).onError<PaperlessServerException>(
|
||||
]).onError<PaperlessApiException>(
|
||||
(error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
return [];
|
||||
@@ -105,7 +108,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
listener: (context, state) {
|
||||
try {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
},
|
||||
@@ -197,7 +200,10 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isEmpty) {
|
||||
return const SliverSearchBar(floating: true);
|
||||
return SliverSearchBar(
|
||||
floating: true,
|
||||
titleText: S.of(context)!.documents,
|
||||
);
|
||||
} else {
|
||||
return DocumentSelectionSliverAppBar(
|
||||
state: state,
|
||||
@@ -226,7 +232,9 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(text: S.of(context)!.documents),
|
||||
Tab(text: S.of(context)!.views),
|
||||
if (LocalUserAccount.current.paperlessUser
|
||||
.canViewSavedViews)
|
||||
Tab(text: S.of(context)!.views),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -268,14 +276,16 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildSavedViewsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (LocalUserAccount
|
||||
.current.paperlessUser.canViewSavedViews)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildSavedViewsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -334,7 +344,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
context
|
||||
.read<DocumentsCubit>()
|
||||
.loadMore()
|
||||
.onError<PaperlessServerException>(
|
||||
.onError<PaperlessApiException>(
|
||||
(error, stackTrace) => showErrorMessage(
|
||||
context,
|
||||
error,
|
||||
@@ -419,7 +429,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
if (newView != null) {
|
||||
try {
|
||||
await context.read<SavedViewCubit>().add(newView);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -472,7 +482,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
.read<DocumentsCubit>()
|
||||
.updateFilter(filter: filterIntent.filter!);
|
||||
}
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -524,7 +534,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
);
|
||||
},
|
||||
);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -555,7 +565,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
);
|
||||
},
|
||||
);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -586,7 +596,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
);
|
||||
},
|
||||
);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -617,7 +627,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
);
|
||||
},
|
||||
);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -626,7 +636,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
try {
|
||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||
await context.read<DocumentsCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
@@ -635,7 +645,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
try {
|
||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||
await context.read<SavedViewCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,10 +160,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||
initialValue: widget.initialFilter.documentType,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: false,
|
||||
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.documentType,
|
||||
),
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.canCreateDocumentTypes,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,10 +173,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||
initialValue: widget.initialFilter.correspondent,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
allowSelectUnassigned: false,
|
||||
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.correspondent,
|
||||
),
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.canCreateCorrespondents,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -190,10 +186,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||
initialValue: widget.initialFilter.storagePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
allowSelectUnassigned: false,
|
||||
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.storagePath,
|
||||
),
|
||||
canCreateNewLabel:
|
||||
LocalUserAccount.current.paperlessUser.canCreateStoragePaths,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
||||
S.of(context)!.documentsSuccessfullyDeleted,
|
||||
);
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
||||
if (shouldDelete) {
|
||||
try {
|
||||
onDelete(context, label);
|
||||
} on PaperlessServerException catch (error) {
|
||||
} on PaperlessApiException catch (error) {
|
||||
showErrorMessage(context, error);
|
||||
} catch (error, stackTrace) {
|
||||
log("An error occurred!", error: error, stackTrace: stackTrace);
|
||||
|
||||
@@ -24,10 +24,8 @@ class EditCorrespondentPage extends StatelessWidget {
|
||||
context.read<EditLabelCubit>().replaceCorrespondent(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeCorrespondent(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.correspondent,
|
||||
),
|
||||
canDelete:
|
||||
LocalUserAccount.current.paperlessUser.canDeleteCorrespondents,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -22,10 +22,8 @@ class EditDocumentTypePage extends StatelessWidget {
|
||||
context.read<EditLabelCubit>().replaceDocumentType(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeDocumentType(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.documentType,
|
||||
),
|
||||
canDelete:
|
||||
LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ class EditStoragePathPage extends StatelessWidget {
|
||||
context.read<EditLabelCubit>().replaceStoragePath(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeStoragePath(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.storagePath,
|
||||
),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths,
|
||||
additionalFields: [
|
||||
StoragePathAutofillFormBuilderField(
|
||||
name: StoragePath.pathKey,
|
||||
|
||||
@@ -26,10 +26,7 @@ class EditTagPage extends StatelessWidget {
|
||||
context.read<EditLabelCubit>().replaceTag(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeTag(label),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.delete,
|
||||
PermissionTarget.tag,
|
||||
),
|
||||
canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags,
|
||||
additionalFields: [
|
||||
FormBuilderColorPickerField(
|
||||
initialValue: tag.color,
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
@@ -54,7 +53,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||
|
||||
late bool _enableMatchFormField;
|
||||
|
||||
PaperlessValidationErrors _errors = {};
|
||||
Map<String, String> _errors = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -69,7 +68,8 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||
Widget build(BuildContext context) {
|
||||
List<MatchingAlgorithm> selectableMatchingAlgorithmValues =
|
||||
getSelectableMatchingAlgorithmValues(
|
||||
context.watch<ApiVersion>().hasMultiUserSupport);
|
||||
context.watch<ApiVersion>().hasMultiUserSupport,
|
||||
);
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
@@ -168,10 +168,10 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||
final parsed = widget.fromJsonT(mergedJson);
|
||||
final createdLabel = await widget.submitButtonConfig.onSubmit(parsed);
|
||||
Navigator.pop(context, createdLabel);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on PaperlessValidationErrors catch (errors) {
|
||||
setState(() => _errors = errors);
|
||||
} on PaperlessFormValidationException catch (exception) {
|
||||
setState(() => _errors = exception.validationMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@ class HomePage extends StatefulWidget {
|
||||
|
||||
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
int _currentIndex = 0;
|
||||
late Timer _inboxTimer;
|
||||
Timer? _inboxTimer;
|
||||
late final StreamSubscription _shareMediaSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_listenToInboxChanges();
|
||||
|
||||
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser!;
|
||||
@@ -73,13 +73,15 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _listenToInboxChanges() {
|
||||
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
context.read<InboxCubit>().refreshItemsInInboxCount();
|
||||
}
|
||||
});
|
||||
if (LocalUserAccount.current.paperlessUser.canViewTags) {
|
||||
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
context.read<InboxCubit>().refreshItemsInInboxCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -89,7 +91,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
log('App is now in foreground');
|
||||
context.read<ConnectivityCubit>().reload();
|
||||
log("Reloaded device connectivity state");
|
||||
if (!_inboxTimer.isActive) {
|
||||
if (!(_inboxTimer?.isActive ?? true)) {
|
||||
_listenToInboxChanges();
|
||||
}
|
||||
break;
|
||||
@@ -98,7 +100,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
case AppLifecycleState.detached:
|
||||
default:
|
||||
log('App is now in background');
|
||||
_inboxTimer.cancel();
|
||||
_inboxTimer?.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +108,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_inboxTimer.cancel();
|
||||
_inboxTimer?.cancel();
|
||||
_shareMediaSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -158,8 +160,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add, PermissionTarget.document)) {
|
||||
if (!LocalUserAccount.current.paperlessUser.canCreateDocuments) {
|
||||
Fluttertoast.showToast(
|
||||
msg: "You do not have the permissions to upload documents.",
|
||||
);
|
||||
@@ -200,8 +201,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
),
|
||||
label: S.of(context)!.documents,
|
||||
),
|
||||
if (LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add, PermissionTarget.document))
|
||||
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
|
||||
RouteDescription(
|
||||
icon: const Icon(Icons.document_scanner_outlined),
|
||||
selectedIcon: Icon(
|
||||
@@ -218,33 +218,31 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
),
|
||||
label: S.of(context)!.labels,
|
||||
),
|
||||
RouteDescription(
|
||||
icon: const Icon(Icons.inbox_outlined),
|
||||
selectedIcon: Icon(
|
||||
Icons.inbox,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
if (LocalUserAccount.current.paperlessUser.canViewTags)
|
||||
RouteDescription(
|
||||
icon: const Icon(Icons.inbox_outlined),
|
||||
selectedIcon: Icon(
|
||||
Icons.inbox,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
label: S.of(context)!.inbox,
|
||||
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
|
||||
builder: (context, state) {
|
||||
return Badge.count(
|
||||
isLabelVisible: state.itemsInInboxCount > 0,
|
||||
count: state.itemsInInboxCount,
|
||||
child: icon,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
label: S.of(context)!.inbox,
|
||||
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
|
||||
builder: (context, state) {
|
||||
return Badge.count(
|
||||
isLabelVisible: state.itemsInInboxCount > 0,
|
||||
count: state.itemsInInboxCount,
|
||||
child: icon,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
final routes = <Widget>[
|
||||
const DocumentsPage(),
|
||||
if (LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.document,
|
||||
))
|
||||
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
|
||||
const ScannerPage(),
|
||||
const LabelsPage(),
|
||||
const InboxPage(),
|
||||
if (LocalUserAccount.current.paperlessUser.canViewTags) const InboxPage(),
|
||||
];
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
@@ -43,7 +44,14 @@ class HomeRoute extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return GlobalSettingsBuilder(
|
||||
builder: (context, settings) {
|
||||
final currentLocalUserId = settings.currentLoggedInUser!;
|
||||
final currentLocalUserId = settings.currentLoggedInUser;
|
||||
if (currentLocalUserId == null) {
|
||||
// This is the case when the current user logs out of the app.
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
final currentUser =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
||||
.get(currentLocalUserId)!;
|
||||
final apiVersion = ApiVersion(paperlessApiVersion);
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
@@ -104,12 +112,31 @@ class HomeRoute extends StatelessWidget {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ProxyProvider<PaperlessLabelsApi, LabelRepository>(
|
||||
update: (context, value, previous) =>
|
||||
LabelRepository(value)..initialize(),
|
||||
update: (context, value, previous) {
|
||||
final repo = LabelRepository(value);
|
||||
if (currentUser.paperlessUser.canViewCorrespondents) {
|
||||
repo.findAllCorrespondents();
|
||||
}
|
||||
if (currentUser.paperlessUser.canViewDocumentTypes) {
|
||||
repo.findAllDocumentTypes();
|
||||
}
|
||||
if (currentUser.paperlessUser.canViewTags) {
|
||||
repo.findAllTags();
|
||||
}
|
||||
if (currentUser.paperlessUser.canViewStoragePaths) {
|
||||
repo.findAllStoragePaths();
|
||||
}
|
||||
return repo;
|
||||
},
|
||||
),
|
||||
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
|
||||
update: (context, value, previous) =>
|
||||
SavedViewRepository(value)..initialize(),
|
||||
update: (context, value, previous) {
|
||||
final repo = SavedViewRepository(value);
|
||||
if (currentUser.paperlessUser.canViewSavedViews) {
|
||||
repo.initialize();
|
||||
}
|
||||
return repo;
|
||||
},
|
||||
),
|
||||
],
|
||||
builder: (context, child) {
|
||||
|
||||
@@ -38,8 +38,8 @@ class _InboxPageState extends State<InboxPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canEditDocument = LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.change, PermissionTarget.document);
|
||||
final canEditDocument =
|
||||
LocalUserAccount.current.paperlessUser.canEditDocuments;
|
||||
return Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
|
||||
@@ -65,7 +65,9 @@ class _InboxPageState extends State<InboxPage>
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: const SliverSearchBar(),
|
||||
sliver: SliverSearchBar(
|
||||
titleText: S.of(context)!.inbox,
|
||||
),
|
||||
)
|
||||
],
|
||||
body: BlocBuilder<InboxCubit, InboxState>(
|
||||
@@ -222,14 +224,14 @@ class _InboxPageState extends State<InboxPage>
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on ServerMessageException catch (error) {
|
||||
showGenericError(context, error.message);
|
||||
} catch (error) {
|
||||
showErrorMessage(
|
||||
context,
|
||||
const PaperlessServerException.unknown(),
|
||||
const PaperlessApiException.unknown(),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
@@ -243,7 +245,7 @@ class _InboxPageState extends State<InboxPage>
|
||||
await context
|
||||
.read<InboxCubit>()
|
||||
.undoRemoveFromInbox(document, removedTags);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,10 +238,8 @@ class _InboxItemState extends State<InboxItem> {
|
||||
}
|
||||
|
||||
Widget _buildActions(BuildContext context) {
|
||||
final canEdit = LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.change, PermissionTarget.document);
|
||||
final canDelete = LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.delete, PermissionTarget.document);
|
||||
final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments;
|
||||
final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments;
|
||||
final chipShape = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
);
|
||||
|
||||
@@ -73,10 +73,7 @@ class TagsFormField extends StatelessWidget {
|
||||
initialValue: field.value,
|
||||
allowOnlySelection: allowOnlySelection,
|
||||
allowCreation: allowCreation &&
|
||||
LocalUserAccount.current.paperlessUser.hasPermission(
|
||||
PermissionAction.add,
|
||||
PermissionTarget.tag,
|
||||
),
|
||||
LocalUserAccount.current.paperlessUser.canCreateTags,
|
||||
allowExclude: allowExclude,
|
||||
),
|
||||
onClosed: (data) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
@@ -39,291 +42,327 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
late final TabController _tabController;
|
||||
int _currentIndex = 0;
|
||||
|
||||
int _calculateTabCount(UserModel user) => [
|
||||
user.canViewCorrespondents,
|
||||
user.canViewDocumentTypes,
|
||||
user.canViewTags,
|
||||
user.canViewStoragePaths,
|
||||
].fold(0, (value, element) => value + (element ? 1 : 0));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_tabController = TabController(length: 4, vsync: this)
|
||||
final user = LocalUserAccount.current.paperlessUser;
|
||||
_tabController = TabController(
|
||||
length: _calculateTabCount(user), vsync: this)
|
||||
..addListener(() => setState(() => _currentIndex = _tabController.index));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: [
|
||||
_openAddCorrespondentPage,
|
||||
_openAddDocumentTypePage,
|
||||
_openAddTagPage,
|
||||
_openAddStoragePathPage,
|
||||
][_currentIndex],
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: const SliverSearchBar(),
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||
builder: (context, box, child) {
|
||||
final currentUserId =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||
.getValue()!
|
||||
.currentLoggedInUser;
|
||||
final user = box.get(currentUserId)!.paperlessUser;
|
||||
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: [
|
||||
if (user.canViewCorrespondents) _openAddCorrespondentPage,
|
||||
if (user.canViewDocumentTypes) _openAddDocumentTypePage,
|
||||
if (user.canViewTags) _openAddTagPage,
|
||||
if (user.canViewStoragePaths) _openAddStoragePathPage,
|
||||
][_currentIndex],
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
SliverOverlapAbsorber(
|
||||
handle: tabBarHandle,
|
||||
sliver: SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: CustomizableSliverPersistentHeaderDelegate(
|
||||
child: ColoredTabBar(
|
||||
tabBar: TabBar(
|
||||
body: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: SliverSearchBar(
|
||||
titleText: S.of(context)!.labels,
|
||||
),
|
||||
),
|
||||
SliverOverlapAbsorber(
|
||||
handle: tabBarHandle,
|
||||
sliver: SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: CustomizableSliverPersistentHeaderDelegate(
|
||||
child: ColoredTabBar(
|
||||
tabBar: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
if (user.canViewCorrespondents)
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: S.of(context)!.correspondents,
|
||||
child: Icon(
|
||||
Icons.person_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (user.canViewDocumentTypes)
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: S.of(context)!.documentTypes,
|
||||
child: Icon(
|
||||
Icons.description_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (user.canViewTags)
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: S.of(context)!.tags,
|
||||
child: Icon(
|
||||
Icons.label_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (user.canViewStoragePaths)
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: S.of(context)!.storagePaths,
|
||||
child: Icon(
|
||||
Icons.folder_open,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
minExtent: kTextTabBarHeight,
|
||||
maxExtent: kTextTabBarHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: BlocBuilder<LabelCubit, LabelState>(
|
||||
builder: (context, state) {
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final metrics = notification.metrics;
|
||||
if (metrics.maxScrollExtent == 0) {
|
||||
return true;
|
||||
}
|
||||
final desiredTab =
|
||||
((metrics.pixels / metrics.maxScrollExtent) *
|
||||
(_tabController.length - 1))
|
||||
.round();
|
||||
|
||||
if (metrics.axis == Axis.horizontal &&
|
||||
_currentIndex != desiredTab) {
|
||||
setState(() => _currentIndex = desiredTab);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
edgeOffset: kTextTabBarHeight,
|
||||
notificationPredicate: (notification) =>
|
||||
connectedState.isConnected,
|
||||
onRefresh: () async {
|
||||
try {
|
||||
await [
|
||||
context
|
||||
.read<LabelCubit>()
|
||||
.reloadCorrespondents,
|
||||
context
|
||||
.read<LabelCubit>()
|
||||
.reloadDocumentTypes,
|
||||
context.read<LabelCubit>().reloadTags,
|
||||
context.read<LabelCubit>().reloadStoragePaths,
|
||||
][_currentIndex]
|
||||
.call();
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint(
|
||||
"[LabelsPage] RefreshIndicator.onRefresh "
|
||||
"${[
|
||||
"correspondents",
|
||||
"document types",
|
||||
"tags",
|
||||
"storage paths"
|
||||
][_currentIndex]}: "
|
||||
"An error occurred (${error.toString()})",
|
||||
);
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
}
|
||||
},
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.person_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
children: [
|
||||
if (user.canViewCorrespondents)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<Correspondent>(
|
||||
labels: state.correspondents,
|
||||
filterBuilder: (label) =>
|
||||
DocumentFilter(
|
||||
correspondent:
|
||||
IdQueryParameter.fromId(
|
||||
label.id!),
|
||||
),
|
||||
canEdit: user.canEditCorrespondents,
|
||||
canAddNew:
|
||||
user.canCreateCorrespondents,
|
||||
onEdit: _openEditCorrespondentPage,
|
||||
emptyStateActionButtonLabel: S
|
||||
.of(context)!
|
||||
.addNewCorrespondent,
|
||||
emptyStateDescription: S
|
||||
.of(context)!
|
||||
.noCorrespondentsSetUp,
|
||||
onAddNew: _openAddCorrespondentPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.description_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
if (user.canViewDocumentTypes)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<DocumentType>(
|
||||
labels: state.documentTypes,
|
||||
filterBuilder: (label) =>
|
||||
DocumentFilter(
|
||||
documentType:
|
||||
IdQueryParameter.fromId(
|
||||
label.id!),
|
||||
),
|
||||
canEdit: user.canEditDocumentTypes,
|
||||
canAddNew:
|
||||
user.canCreateDocumentTypes,
|
||||
onEdit: _openEditDocumentTypePage,
|
||||
emptyStateActionButtonLabel: S
|
||||
.of(context)!
|
||||
.addNewDocumentType,
|
||||
emptyStateDescription: S
|
||||
.of(context)!
|
||||
.noDocumentTypesSetUp,
|
||||
onAddNew: _openAddDocumentTypePage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.label_outline,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
if (user.canViewTags)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<Tag>(
|
||||
labels: state.tags,
|
||||
filterBuilder: (label) =>
|
||||
DocumentFilter(
|
||||
tags: TagsQuery.ids(
|
||||
include: [label.id!]),
|
||||
),
|
||||
canEdit: user.canEditTags,
|
||||
canAddNew: user.canCreateTags,
|
||||
onEdit: _openEditTagPage,
|
||||
leadingBuilder: (t) => CircleAvatar(
|
||||
backgroundColor: t.color,
|
||||
child: t.isInboxTag
|
||||
? Icon(
|
||||
Icons.inbox,
|
||||
color: t.textColor,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewTag,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noTagsSetUp,
|
||||
onAddNew: _openAddTagPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.folder_open,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
if (user.canViewStoragePaths)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle),
|
||||
LabelTabView<StoragePath>(
|
||||
labels: state.storagePaths,
|
||||
onEdit: _openEditStoragePathPage,
|
||||
filterBuilder: (label) =>
|
||||
DocumentFilter(
|
||||
storagePath:
|
||||
IdQueryParameter.fromId(
|
||||
label.id!),
|
||||
),
|
||||
canEdit: user.canEditStoragePaths,
|
||||
canAddNew:
|
||||
user.canCreateStoragePaths,
|
||||
contentBuilder: (path) =>
|
||||
Text(path.path),
|
||||
emptyStateActionButtonLabel: S
|
||||
.of(context)!
|
||||
.addNewStoragePath,
|
||||
emptyStateDescription: S
|
||||
.of(context)!
|
||||
.noStoragePathsSetUp,
|
||||
onAddNew: _openAddStoragePathPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
minExtent: kTextTabBarHeight,
|
||||
maxExtent: kTextTabBarHeight),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
body: BlocBuilder<LabelCubit, LabelState>(
|
||||
builder: (context, state) {
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final metrics = notification.metrics;
|
||||
if (metrics.maxScrollExtent == 0) {
|
||||
return true;
|
||||
}
|
||||
final desiredTab =
|
||||
((metrics.pixels / metrics.maxScrollExtent) *
|
||||
(_tabController.length - 1))
|
||||
.round();
|
||||
|
||||
if (metrics.axis == Axis.horizontal &&
|
||||
_currentIndex != desiredTab) {
|
||||
setState(() => _currentIndex = desiredTab);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
edgeOffset: kTextTabBarHeight,
|
||||
notificationPredicate: (notification) =>
|
||||
connectedState.isConnected,
|
||||
onRefresh: () async {
|
||||
try {
|
||||
await [
|
||||
context.read<LabelCubit>().reloadCorrespondents,
|
||||
context.read<LabelCubit>().reloadDocumentTypes,
|
||||
context.read<LabelCubit>().reloadTags,
|
||||
context.read<LabelCubit>().reloadStoragePaths,
|
||||
][_currentIndex]
|
||||
.call();
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint(
|
||||
"[LabelsPage] RefreshIndicator.onRefresh "
|
||||
"${[
|
||||
"correspondents",
|
||||
"document types",
|
||||
"tags",
|
||||
"storage paths"
|
||||
][_currentIndex]}: "
|
||||
"An error occurred (${error.toString()})",
|
||||
);
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
}
|
||||
},
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<Correspondent>(
|
||||
labels: state.correspondents,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
correspondent:
|
||||
IdQueryParameter.fromId(label.id!),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.correspondent),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.correspondent),
|
||||
onEdit: _openEditCorrespondentPage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewCorrespondent,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noCorrespondentsSetUp,
|
||||
onAddNew: _openAddCorrespondentPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<DocumentType>(
|
||||
labels: state.documentTypes,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
documentType:
|
||||
IdQueryParameter.fromId(label.id!),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.documentType),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.documentType),
|
||||
onEdit: _openEditDocumentTypePage,
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewDocumentType,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noDocumentTypesSetUp,
|
||||
onAddNew: _openAddDocumentTypePage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<Tag>(
|
||||
labels: state.tags,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
tags:
|
||||
TagsQuery.ids(include: [label.id!]),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.tag),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.tag),
|
||||
onEdit: _openEditTagPage,
|
||||
leadingBuilder: (t) => CircleAvatar(
|
||||
backgroundColor: t.color,
|
||||
child: t.isInboxTag
|
||||
? Icon(
|
||||
Icons.inbox,
|
||||
color: t.textColor,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewTag,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noTagsSetUp,
|
||||
onAddNew: _openAddTagPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
LabelTabView<StoragePath>(
|
||||
labels: state.storagePaths,
|
||||
onEdit: _openEditStoragePathPage,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
storagePath:
|
||||
IdQueryParameter.fromId(label.id!),
|
||||
),
|
||||
canEdit: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(
|
||||
PermissionAction.change,
|
||||
PermissionTarget.storagePath),
|
||||
canAddNew: LocalUserAccount
|
||||
.current.paperlessUser
|
||||
.hasPermission(PermissionAction.add,
|
||||
PermissionTarget.storagePath),
|
||||
contentBuilder: (path) => Text(path.path),
|
||||
emptyStateActionButtonLabel:
|
||||
S.of(context)!.addNewStoragePath,
|
||||
emptyStateDescription:
|
||||
S.of(context)!.noStoragePathsSetUp,
|
||||
onAddNew: _openAddStoragePathPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _openEditCorrespondentPage(Correspondent correspondent) {
|
||||
|
||||
@@ -36,8 +36,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
||||
|
||||
Widget _buildReferencedDocumentsWidget(BuildContext context) {
|
||||
final canOpen = (label.documentCount ?? 0) > 0 &&
|
||||
LocalUserAccount.current.paperlessUser
|
||||
.hasPermission(PermissionAction.view, PermissionTarget.document);
|
||||
LocalUserAccount.current.paperlessUser.canViewDocuments;
|
||||
return TextButton.icon(
|
||||
label: const Icon(Icons.link),
|
||||
icon: Text(formatMaxCount(label.documentCount)),
|
||||
|
||||
@@ -365,7 +365,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
apiVersion: apiVersion,
|
||||
)
|
||||
.findCurrentUser();
|
||||
} on DioError catch (error, stackTrace) {
|
||||
} on DioException catch (error, stackTrace) {
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"An error occurred: ${error.message}",
|
||||
|
||||
@@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
),
|
||||
ServerConnectionPage(
|
||||
titleString: widget.titleString,
|
||||
titleText: widget.titleString,
|
||||
formBuilderKey: _formKey,
|
||||
onContinue: () {
|
||||
_pageController.nextPage(
|
||||
@@ -126,7 +126,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
|
||||
FocusScope.of(context).unfocus();
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final form = _formKey.currentState!.value;
|
||||
@@ -150,7 +149,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
form[ServerAddressFormField.fkServerAddress],
|
||||
clientCert,
|
||||
);
|
||||
} on PaperlessServerException catch (error) {
|
||||
} on PaperlessApiException catch (error) {
|
||||
showErrorMessage(context, error);
|
||||
} on ServerMessageException catch (error) {
|
||||
showLocalizedError(context, error.message);
|
||||
|
||||
@@ -66,7 +66,10 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
.values
|
||||
.where((element) => element.contains(textEditingValue.text));
|
||||
},
|
||||
onSelected: (option) => _formatInput(),
|
||||
onSelected: (option) {
|
||||
_formatInput();
|
||||
field.didChange(_textEditingController.text);
|
||||
},
|
||||
fieldViewBuilder:
|
||||
(context, textEditingController, focusNode, onFieldSubmitted) {
|
||||
return TextField(
|
||||
@@ -111,6 +114,10 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
String address = _textEditingController.text.trim();
|
||||
address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
|
||||
_textEditingController.text = address;
|
||||
_textEditingController.selection = TextSelection(
|
||||
baseOffset: address.length,
|
||||
extentOffset: address.length,
|
||||
);
|
||||
widget.onSubmit(address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ import 'package:provider/provider.dart';
|
||||
|
||||
class ServerConnectionPage extends StatefulWidget {
|
||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
||||
final void Function() onContinue;
|
||||
final String titleString;
|
||||
final VoidCallback onContinue;
|
||||
final String titleText;
|
||||
|
||||
const ServerConnectionPage({
|
||||
super.key,
|
||||
required this.formBuilderKey,
|
||||
required this.onContinue,
|
||||
required this.titleString,
|
||||
required this.titleText,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -36,7 +36,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
toolbarHeight: kToolbarHeight - 4,
|
||||
title: Text(widget.titleString),
|
||||
title: Text(widget.titleText),
|
||||
bottom: PreferredSize(
|
||||
child: _isCheckingConnection
|
||||
? const LinearProgressIndicator()
|
||||
|
||||
@@ -26,7 +26,7 @@ mixin DocumentPagingViewMixin<T extends StatefulWidget,
|
||||
if (shouldLoadMoreDocuments) {
|
||||
try {
|
||||
await _bloc.loadMore();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
|
||||
super.initState();
|
||||
try {
|
||||
context.read<SimilarDocumentsCubit>().initialize();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ void showLocalizedError(
|
||||
|
||||
void showErrorMessage(
|
||||
BuildContext context,
|
||||
PaperlessServerException error, [
|
||||
PaperlessApiException error, [
|
||||
StackTrace? stackTrace,
|
||||
]) {
|
||||
showSnackBar(
|
||||
|
||||
@@ -30,7 +30,6 @@ import 'package:paperless_mobile/core/interceptor/language_header.interceptor.da
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
||||
import 'package:paperless_mobile/features/home/view/home_route.dart';
|
||||
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
|
||||
@@ -168,7 +167,7 @@ void main() async {
|
||||
);
|
||||
}, (error, stack) {
|
||||
String message = switch (error) {
|
||||
PaperlessServerException e => e.details ?? error.toString(),
|
||||
PaperlessApiException e => e.details ?? error.toString(),
|
||||
ServerMessageException e => e.message,
|
||||
_ => error.toString()
|
||||
};
|
||||
@@ -315,8 +314,10 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
) async {
|
||||
try {
|
||||
await context.read<AuthenticationCubit>().login(
|
||||
credentials:
|
||||
LoginFormCredentials(username: username, password: password),
|
||||
credentials: LoginFormCredentials(
|
||||
username: username,
|
||||
password: password,
|
||||
),
|
||||
serverUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
);
|
||||
@@ -335,13 +336,17 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
globalSettings.save();
|
||||
});
|
||||
}
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
} on PaperlessApiException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on PaperlessValidationErrors catch (error, stackTrace) {
|
||||
if (error.hasFieldUnspecificError) {
|
||||
showLocalizedError(context, error.fieldUnspecificError!);
|
||||
} on PaperlessFormValidationException catch (exception, stackTrace) {
|
||||
if (exception.hasUnspecificErrorMessage()) {
|
||||
showLocalizedError(context, exception.unspecificErrorMessage()!);
|
||||
} else {
|
||||
showGenericError(context, error.values.first, stackTrace);
|
||||
showGenericError(
|
||||
context,
|
||||
exception.validationMessages.values.first,
|
||||
stackTrace,
|
||||
); //TODO: Check if we can show error message directly on field here.
|
||||
}
|
||||
} catch (unknownError, stackTrace) {
|
||||
showGenericError(context, unknownError.toString(), stackTrace);
|
||||
|
||||
Reference in New Issue
Block a user