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:
Anton Stubenbord
2023-07-22 14:17:48 +02:00
parent c4f2810974
commit 6566b2b8d7
70 changed files with 1446 additions and 1133 deletions

View File

@@ -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;
}

View 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;
}

View 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);
}
}
}

View File

@@ -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',

View File

@@ -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,
));
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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.",
};
}

View File

@@ -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'];
}

View File

@@ -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");

View File

@@ -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

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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),
),
],
),
);

View File

@@ -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);

View File

@@ -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(() {

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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(),
),
);
},
),
),
],
);
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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,
);
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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,
);
}),
);

View File

@@ -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,
),
);
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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: [

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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),
);

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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)),

View File

@@ -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}",

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -92,7 +92,7 @@ void showLocalizedError(
void showErrorMessage(
BuildContext context,
PaperlessServerException error, [
PaperlessApiException error, [
StackTrace? stackTrace,
]) {
showSnackBar(

View File

@@ -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);