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),
),
);
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(
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,11 +120,13 @@ 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(
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,12 +198,20 @@ class _DocumentUploadPreparationPageState
),
),
// Correspondent
if (LocalUserAccount
.current.paperlessUser.canViewCorrespondents)
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
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,
@@ -211,19 +220,23 @@ class _DocumentUploadPreparationPageState
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateCorrespondents,
),
// Document type
if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
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,
@@ -232,12 +245,10 @@ class _DocumentUploadPreparationPageState
options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateDocumentTypes,
),
if (LocalUserAccount.current.paperlessUser.canViewTags)
TagsFormField(
name: DocumentModel.tagsKey,
allowCreation: true,
@@ -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,6 +232,8 @@ class _DocumentsPageState extends State<DocumentsPage>
controller: _tabController,
tabs: [
Tab(text: S.of(context)!.documents),
if (LocalUserAccount.current.paperlessUser
.canViewSavedViews)
Tab(text: S.of(context)!.views),
],
),
@@ -268,6 +276,8 @@ class _DocumentsPageState extends State<DocumentsPage>
);
},
),
if (LocalUserAccount
.current.paperlessUser.canViewSavedViews)
Builder(
builder: (context) {
return _buildSavedViewsTab(
@@ -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,6 +73,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
void _listenToInboxChanges() {
if (LocalUserAccount.current.paperlessUser.canViewTags) {
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
if (!mounted) {
timer.cancel();
@@ -81,6 +82,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
});
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
@@ -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,6 +218,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
label: S.of(context)!.labels,
),
if (LocalUserAccount.current.paperlessUser.canViewTags)
RouteDescription(
icon: const Icon(Icons.inbox_outlined),
selectedIcon: Icon(
@@ -238,13 +239,10 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
];
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,29 +42,45 @@ 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>(
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: [
_openAddCorrespondentPage,
_openAddDocumentTypePage,
_openAddTagPage,
_openAddStoragePathPage,
if (user.canViewCorrespondents) _openAddCorrespondentPage,
if (user.canViewDocumentTypes) _openAddDocumentTypePage,
if (user.canViewTags) _openAddTagPage,
if (user.canViewStoragePaths) _openAddStoragePathPage,
][_currentIndex],
child: const Icon(Icons.add),
),
@@ -70,7 +89,9 @@ class _LabelsPageState extends State<LabelsPage>
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: const SliverSearchBar(),
sliver: SliverSearchBar(
titleText: S.of(context)!.labels,
),
),
SliverOverlapAbsorber(
handle: tabBarHandle,
@@ -81,43 +102,60 @@ class _LabelsPageState extends State<LabelsPage>
tabBar: TabBar(
controller: _tabController,
tabs: [
if (user.canViewCorrespondents)
Tab(
icon: Icon(
icon: Tooltip(
message: S.of(context)!.correspondents,
child: Icon(
Icons.person_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewDocumentTypes)
Tab(
icon: Icon(
icon: Tooltip(
message: S.of(context)!.documentTypes,
child: Icon(
Icons.description_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewTags)
Tab(
icon: Icon(
icon: Tooltip(
message: S.of(context)!.tags,
child: Icon(
Icons.label_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewStoragePaths)
Tab(
icon: Icon(
icon: Tooltip(
message: S.of(context)!.storagePaths,
child: Icon(
Icons.folder_open,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
],
),
),
minExtent: kTextTabBarHeight,
maxExtent: kTextTabBarHeight),
maxExtent: kTextTabBarHeight,
),
),
),
],
@@ -147,8 +185,12 @@ class _LabelsPageState extends State<LabelsPage>
onRefresh: () async {
try {
await [
context.read<LabelCubit>().reloadCorrespondents,
context.read<LabelCubit>().reloadDocumentTypes,
context
.read<LabelCubit>()
.reloadCorrespondents,
context
.read<LabelCubit>()
.reloadDocumentTypes,
context.read<LabelCubit>().reloadTags,
context.read<LabelCubit>().reloadStoragePaths,
][_currentIndex]
@@ -170,94 +212,90 @@ class _LabelsPageState extends State<LabelsPage>
child: TabBarView(
controller: _tabController,
children: [
if (user.canViewCorrespondents)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: state.correspondents,
filterBuilder: (label) => DocumentFilter(
filterBuilder: (label) =>
DocumentFilter(
correspondent:
IdQueryParameter.fromId(label.id!),
IdQueryParameter.fromId(
label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.correspondent),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.correspondent),
canEdit: user.canEditCorrespondents,
canAddNew:
user.canCreateCorrespondents,
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel:
S.of(context)!.addNewCorrespondent,
emptyStateDescription:
S.of(context)!.noCorrespondentsSetUp,
emptyStateActionButtonLabel: S
.of(context)!
.addNewCorrespondent,
emptyStateDescription: S
.of(context)!
.noCorrespondentsSetUp,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
),
if (user.canViewDocumentTypes)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: state.documentTypes,
filterBuilder: (label) => DocumentFilter(
filterBuilder: (label) =>
DocumentFilter(
documentType:
IdQueryParameter.fromId(label.id!),
IdQueryParameter.fromId(
label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.documentType),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.documentType),
canEdit: user.canEditDocumentTypes,
canAddNew:
user.canCreateDocumentTypes,
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel:
S.of(context)!.addNewDocumentType,
emptyStateDescription:
S.of(context)!.noDocumentTypesSetUp,
emptyStateActionButtonLabel: S
.of(context)!
.addNewDocumentType,
emptyStateDescription: S
.of(context)!
.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
),
if (user.canViewTags)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Tag>(
labels: state.tags,
filterBuilder: (label) => DocumentFilter(
tags:
TagsQuery.ids(include: [label.id!]),
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),
canEdit: user.canEditTags,
canAddNew: user.canCreateTags,
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
@@ -278,34 +316,35 @@ class _LabelsPageState extends State<LabelsPage>
);
},
),
if (user.canViewStoragePaths)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: state.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
filterBuilder: (label) =>
DocumentFilter(
storagePath:
IdQueryParameter.fromId(label.id!),
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,
canEdit: user.canEditStoragePaths,
canAddNew:
user.canCreateStoragePaths,
contentBuilder: (path) =>
Text(path.path),
emptyStateActionButtonLabel: S
.of(context)!
.addNewStoragePath,
emptyStateDescription: S
.of(context)!
.noStoragePathsSetUp,
onAddNew: _openAddStoragePathPage,
),
],
@@ -322,8 +361,8 @@ class _LabelsPageState extends State<LabelsPage>
),
);
},
),
);
});
}
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);

View File

@@ -0,0 +1,7 @@
import 'package:dio/dio.dart';
extension DioExceptionUnravelExtension on DioException {
Object unravel({Object? orElse}) {
return error ?? orElse ?? Exception("Unknown");
}
}

View File

@@ -0,0 +1,3 @@
export 'paperless_server_message_exception.dart';
export 'paperless_form_validation_exception.dart';
export 'paperless_unauthorized_exception.dart';

View File

@@ -0,0 +1,42 @@
class PaperlessFormValidationException implements Exception {
final Map<String, String> validationMessages;
PaperlessFormValidationException(this.validationMessages);
bool hasMessageForField(String formKey) {
return validationMessages.containsKey(formKey);
}
bool hasUnspecificErrorMessage() {
return validationMessages.containsKey("non_field_errors");
}
String? unspecificErrorMessage() {
return validationMessages["non_field_errors"];
}
String? messageForField(String formKey) {
return validationMessages[formKey];
}
static bool canParse(Map<String, dynamic> json) {
return json.values.every((element) => element is String);
}
factory PaperlessFormValidationException.fromJson(Map<String, dynamic> json) {
final Map<String, String> validationMessages = {};
for (final entry in json.entries) {
if (entry.value is List) {
validationMessages.putIfAbsent(
entry.key,
() => (entry.value as List).first as String,
);
} else if (entry.value is String) {
validationMessages.putIfAbsent(entry.key, () => entry.value);
} else {
validationMessages.putIfAbsent(entry.key, () => entry.value.toString());
}
}
return PaperlessFormValidationException(validationMessages);
}
}

View File

@@ -0,0 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
part 'paperless_server_exception.g.dart';
@JsonSerializable(createToJson: false)
class PaperlessServerMessageException implements Exception {
final String detail;
PaperlessServerMessageException(this.detail);
static bool canParse(Map<String, dynamic> json) {
return json.containsKey('detail') && json.length == 1;
}
factory PaperlessServerMessageException.fromJson(Map<String, dynamic> json) =>
_$PaperlessServerExceptionFromJson(json);
}

View File

@@ -0,0 +1,5 @@
class PaperlessUnauthorizedException implements Exception {
final String? message;
PaperlessUnauthorizedException(this.message);
}

View File

@@ -0,0 +1 @@

View File

@@ -12,7 +12,7 @@ export 'labels/matching_algorithm.dart';
export 'labels/storage_path_model.dart';
export 'labels/tag_model.dart';
export 'paged_search_result.dart';
export 'paperless_server_exception.dart';
export 'paperless_api_exception.dart';
export 'paperless_server_information_model.dart';
export 'paperless_server_statistics_model.dart';
export 'permissions/inherited_permissions.dart';
@@ -31,3 +31,4 @@ export 'saved_view_model.dart';
export 'task/task.dart';
export 'task/task_status.dart';
export 'user_model.dart';
export 'exception/exceptions.dart';

View File

@@ -1,17 +1,17 @@
class PaperlessServerException implements Exception {
class PaperlessApiException implements Exception {
final ErrorCode code;
final String? details;
final StackTrace? stackTrace;
final int? httpStatusCode;
const PaperlessServerException(
const PaperlessApiException(
this.code, {
this.details,
this.stackTrace,
this.httpStatusCode,
});
const PaperlessServerException.unknown() : this(ErrorCode.unknown);
const PaperlessApiException.unknown() : this(ErrorCode.unknown);
@override
String toString() {
@@ -53,5 +53,6 @@ enum ErrorCode {
requestTimedOut,
unsupportedFileFormat,
missingClientCertificate,
acknowledgeTasksError;
acknowledgeTasksError,
correspondentDeleteFailed, documentTypeDeleteFailed, tagDeleteFailed, correspondentUpdateFailed, documentTypeUpdateFailed, tagUpdateFailed, storagePathDeleteFailed, storagePathUpdateFailed, serverInformationLoadFailed, serverStatisticsLoadFailed, uiSettingsLoadFailed, loadTasksError, userNotFound;
}

View File

@@ -6,13 +6,15 @@ extension UserPermissionExtension on UserModel {
v3: (user) {
final permission = [action.value, target.value].join("_");
return user.userPermissions.any((element) => element == permission) ||
user.inheritedPermissions.any((element) => element.split(".").last == permission);
user.inheritedPermissions
.any((element) => element.split(".").last == permission);
},
v2: (_) => true,
);
}
bool hasPermissions(List<PermissionAction> actions, List<PermissionTarget> targets) {
bool hasPermissions(
List<PermissionAction> actions, List<PermissionTarget> targets) {
return map(
v3: (user) {
final permissions = [
@@ -21,10 +23,62 @@ extension UserPermissionExtension on UserModel {
];
return permissions.every((requestedPermission) =>
user.userPermissions.contains(requestedPermission) ||
user.inheritedPermissions
.any((element) => element.split(".").last == requestedPermission));
user.inheritedPermissions.any(
(element) => element.split(".").last == requestedPermission));
},
v2: (_) => true,
);
}
bool get canViewDocuments =>
hasPermission(PermissionAction.view, PermissionTarget.document);
bool get canViewCorrespondents =>
hasPermission(PermissionAction.view, PermissionTarget.correspondent);
bool get canViewDocumentTypes =>
hasPermission(PermissionAction.view, PermissionTarget.documentType);
bool get canViewTags =>
hasPermission(PermissionAction.view, PermissionTarget.tag);
bool get canViewStoragePaths =>
hasPermission(PermissionAction.view, PermissionTarget.storagePath);
bool get canViewSavedViews =>
hasPermission(PermissionAction.view, PermissionTarget.savedView);
bool get canEditDocuments =>
hasPermission(PermissionAction.change, PermissionTarget.document);
bool get canEditCorrespondents =>
hasPermission(PermissionAction.change, PermissionTarget.correspondent);
bool get canEditDocumentTypes =>
hasPermission(PermissionAction.change, PermissionTarget.documentType);
bool get canEditTags =>
hasPermission(PermissionAction.change, PermissionTarget.tag);
bool get canEditStoragePaths =>
hasPermission(PermissionAction.change, PermissionTarget.storagePath);
bool get canEditavedViews =>
hasPermission(PermissionAction.change, PermissionTarget.savedView);
bool get canDeleteDocuments =>
hasPermission(PermissionAction.delete, PermissionTarget.document);
bool get canDeleteCorrespondents =>
hasPermission(PermissionAction.delete, PermissionTarget.correspondent);
bool get canDeleteDocumentTypes =>
hasPermission(PermissionAction.delete, PermissionTarget.documentType);
bool get canDeleteTags =>
hasPermission(PermissionAction.delete, PermissionTarget.tag);
bool get canDeleteStoragePaths =>
hasPermission(PermissionAction.delete, PermissionTarget.storagePath);
bool get canDeleteSavedViews =>
hasPermission(PermissionAction.delete, PermissionTarget.savedView);
bool get canCreateDocuments =>
hasPermission(PermissionAction.add, PermissionTarget.document);
bool get canCreateCorrespondents =>
hasPermission(PermissionAction.add, PermissionTarget.correspondent);
bool get canCreateDocumentTypes =>
hasPermission(PermissionAction.add, PermissionTarget.documentType);
bool get canCreateTags =>
hasPermission(PermissionAction.add, PermissionTarget.tag);
bool get canCreateStoragePaths =>
hasPermission(PermissionAction.add, PermissionTarget.storagePath);
bool get canCreateSavedViews =>
hasPermission(PermissionAction.add, PermissionTarget.savedView);
}

View File

@@ -1,4 +1,9 @@
import 'package:paperless_api/src/models/exception/exceptions.dart';
abstract class PaperlessAuthenticationApi {
///
/// @throws [PaperlessUnauthorizedException]
///
Future<String> login({
required String username,
required String password,

View File

@@ -1,6 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart';
class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
@@ -13,34 +12,20 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
required String username,
required String password,
}) async {
late Response response;
try {
response = await client.post(
final response = await client.post(
"/api/token/",
data: {
"username": username,
"password": password,
},
options: Options(
validateStatus: (status) => status == 200,
),
);
} on DioError catch (error) {
if (error.error is PaperlessServerException ||
error.error is Map<String, String>) {
throw error.error as Map<String, String>;
} else {
throw PaperlessServerException(
ErrorCode.authenticationFailed,
details: error.message,
);
}
}
if (response.statusCode == 200) {
return response.data['token'];
} else {
throw PaperlessServerException(
ErrorCode.authenticationFailed,
httpStatusCode: response.statusCode,
);
} on DioException catch (exception) {
throw exception.unravel();
}
}
}

View File

@@ -18,7 +18,7 @@ abstract class PaperlessDocumentsApi {
Future<DocumentModel> update(DocumentModel doc);
Future<int> findNextAsn();
Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter);
Future<DocumentModel?> find(int id);
Future<DocumentModel> find(int id);
Future<int> delete(DocumentModel doc);
Future<DocumentMetaData> getMetaData(DocumentModel document);
Future<Iterable<int>> bulkAction(BulkAction action);

View File

@@ -4,6 +4,8 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final Dio client;
@@ -55,20 +57,17 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
onSendProgress: (count, total) {
debugPrint("Uploading ${(count / total) * 100}%...");
},
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == 200) {
if (response.data is String && response.data != "OK") {
return response.data;
}
return null;
if (response.data != "OK") {
return response.data as String;
} else {
throw PaperlessServerException(
ErrorCode.documentUploadFailed,
httpStatusCode: response.statusCode,
);
return null;
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentUploadFailed),
);
}
}
@@ -78,14 +77,13 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final response = await client.put(
"/api/documents/${doc.id}/",
data: doc.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == 200) {
return DocumentModel.fromJson(response.data);
} else {
throw const PaperlessServerException(ErrorCode.documentUpdateFailed);
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentUpdateFailed),
);
}
}
@@ -93,13 +91,14 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
Future<PagedSearchResult<DocumentModel>> findAll(
DocumentFilter filter,
) async {
final filterParams = filter.toQueryParameters()..addAll({'truncate_content': "true"});
final filterParams = filter.toQueryParameters()
..addAll({'truncate_content': "true"});
try {
final response = await client.get(
"/api/documents/",
queryParameters: filterParams,
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == 200) {
return compute(
PagedSearchResult.fromJsonSingleParam,
PagedSearchResultJsonSerializer<DocumentModel>(
@@ -107,25 +106,26 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
DocumentModelJsonConverter(),
),
);
} else {
throw const PaperlessServerException(ErrorCode.documentLoadFailed);
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentLoadFailed),
);
}
}
@override
Future<int> delete(DocumentModel doc) async {
try {
final response = await client.delete("/api/documents/${doc.id}/");
await client.delete(
"/api/documents/${doc.id}/",
options: Options(validateStatus: (status) => status == 204),
);
if (response.statusCode == 204) {
return Future.value(doc.id);
}
throw const PaperlessServerException(ErrorCode.documentDeleteFailed);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentDeleteFailed),
);
}
}
@@ -143,15 +143,16 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
try {
final response = await client.get(
getPreviewUrl(documentId),
options:
Options(responseType: ResponseType.bytes), //TODO: Check if bytes or stream is required
options: Options(
responseType: ResponseType.bytes,
validateStatus: (status) => status == 200,
), //TODO: Check if bytes or stream is required
);
if (response.statusCode == 200) {
return response.data;
}
throw const PaperlessServerException(ErrorCode.documentPreviewFailed);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentPreviewFailed),
);
}
}
@@ -170,30 +171,31 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
.map((e) => e.archiveSerialNumber)
.firstWhere((asn) => asn != null, orElse: () => 0)! +
1;
} on PaperlessServerException {
throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on PaperlessApiException {
throw const PaperlessApiException(ErrorCode.documentAsnQueryFailed);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentAsnQueryFailed),
);
}
}
@override
Future<Iterable<int>> bulkAction(BulkAction action) async {
try {
final response = await client.post(
await client.post(
"/api/documents/bulk_edit/",
data: action.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == 200) {
return action.documentIds;
} else {
throw const PaperlessServerException(
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentBulkActionFailed,
),
);
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
}
}
@override
@@ -208,8 +210,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
options: Options(responseType: ResponseType.bytes),
);
return response.data;
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
@@ -224,25 +228,31 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final response = await client.download(
"/api/documents/${document.id}/download/",
localFilePath,
onReceiveProgress: (count, total) => onProgressChanged?.call(count / total),
onReceiveProgress: (count, total) =>
onProgressChanged?.call(count / total),
queryParameters: {'original': original},
);
return response.data;
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
@override
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
try {
final response = await client.get("/api/documents/${document.id}/metadata/");
final response =
await client.get("/api/documents/${document.id}/metadata/");
return compute(
DocumentMetaData.fromJson,
response.data as Map<String, dynamic>,
);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
@@ -255,40 +265,46 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
'term': query,
'limit': limit,
},
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == 200) {
return (response.data as List).cast<String>();
}
throw const PaperlessServerException(ErrorCode.autocompleteQueryError);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.autocompleteQueryError,
),
);
}
}
@override
Future<FieldSuggestions> findSuggestions(DocumentModel document) async {
try {
final response = await client.get("/api/documents/${document.id}/suggestions/");
if (response.statusCode == 200) {
return FieldSuggestions.fromJson(response.data).forDocumentId(document.id);
}
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
final response = await client.get(
"/api/documents/${document.id}/suggestions/",
options: Options(validateStatus: (status) => status == 200),
);
return FieldSuggestions.fromJson(response.data)
.forDocumentId(document.id);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.suggestionsQueryError),
);
}
}
@override
Future<DocumentModel?> find(int id) async {
Future<DocumentModel> find(int id) async {
try {
final response = await client.get("/api/documents/$id/");
if (response.statusCode == 200) {
final response = await client.get(
"/api/documents/$id/",
options: Options(validateStatus: (status) => status == 200),
);
return DocumentModel.fromJson(response.data);
} else {
return null;
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
}

View File

@@ -2,11 +2,12 @@ import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/labels/correspondent_model.dart';
import 'package:paperless_api/src/models/labels/document_type_model.dart';
import 'package:paperless_api/src/models/labels/storage_path_model.dart';
import 'package:paperless_api/src/models/labels/tag_model.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart';
import 'package:paperless_api/src/request_utils.dart';
@@ -94,16 +95,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/correspondents/',
data: correspondent.toJson(),
options: Options(validateStatus: (status) => status == 201),
);
if (response.statusCode == HttpStatus.created) {
return Correspondent.fromJson(response.data);
}
throw PaperlessServerException(
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.correspondentCreateFailed,
httpStatusCode: response.statusCode,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -113,16 +113,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/document_types/',
data: type.toJson(),
options: Options(
validateStatus: (status) => status == 201,
),
);
if (response.statusCode == HttpStatus.created) {
return DocumentType.fromJson(response.data);
}
throw PaperlessServerException(
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentTypeCreateFailed,
httpStatusCode: response.statusCode,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -132,17 +133,18 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/tags/',
data: tag.toJson(),
options: Options(headers: {"Accept": "application/json; version=2"}),
options: Options(
headers: {"Accept": "application/json; version=2"},
validateStatus: (status) => status == 201,
),
);
if (response.statusCode == HttpStatus.created) {
return Tag.fromJson(response.data);
}
throw PaperlessServerException(
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.tagCreateFailed,
httpStatusCode: response.statusCode,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -150,17 +152,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteCorrespondent(Correspondent correspondent) async {
assert(correspondent.id != null);
try {
final response =
await _client.delete('/api/correspondents/${correspondent.id}/');
if (response.statusCode == HttpStatus.noContent) {
return correspondent.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
await _client.delete(
'/api/correspondents/${correspondent.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return correspondent.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.correspondentDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -168,17 +170,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteDocumentType(DocumentType documentType) async {
assert(documentType.id != null);
try {
final response =
await _client.delete('/api/document_types/${documentType.id}/');
if (response.statusCode == HttpStatus.noContent) {
return documentType.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
final response = await _client.delete(
'/api/document_types/${documentType.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return documentType.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentTypeDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -186,16 +188,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteTag(Tag tag) async {
assert(tag.id != null);
try {
final response = await _client.delete('/api/tags/${tag.id}/');
if (response.statusCode == HttpStatus.noContent) {
return tag.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
await _client.delete(
'/api/tags/${tag.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return tag.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.tagDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -206,16 +209,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.put(
'/api/correspondents/${correspondent.id}/',
data: json.encode(correspondent.toJson()),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == HttpStatus.ok) {
return Correspondent.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown, //TODO: Add correct error code mapping.
httpStatusCode: response.statusCode,
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.correspondentUpdateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -226,16 +228,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.put(
'/api/document_types/${documentType.id}/',
data: documentType.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == HttpStatus.ok) {
return DocumentType.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentTypeUpdateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -245,18 +246,19 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
try {
final response = await _client.put(
'/api/tags/${tag.id}/',
options: Options(headers: {"Accept": "application/json; version=2"}),
options: Options(
headers: {"Accept": "application/json; version=2"},
validateStatus: (status) => status == 200,
),
data: tag.toJson(),
);
if (response.statusCode == HttpStatus.ok) {
return Tag.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.tagUpdateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -264,16 +266,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteStoragePath(StoragePath path) async {
assert(path.id != null);
try {
final response = await _client.delete('/api/storage_paths/${path.id}/');
if (response.statusCode == HttpStatus.noContent) {
return path.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
final response = await _client.delete(
'/api/storage_paths/${path.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return path.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.storagePathDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -307,16 +310,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/storage_paths/',
data: path.toJson(),
options: Options(validateStatus: (status) => status == 201),
);
if (response.statusCode == HttpStatus.created) {
return StoragePath.fromJson(response.data);
}
throw PaperlessServerException(
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.storagePathCreateFailed,
httpStatusCode: response.statusCode,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -327,13 +329,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.put(
'/api/storage_paths/${path.id}/',
data: path.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == HttpStatus.ok) {
return StoragePath.fromJson(response.data);
}
throw const PaperlessServerException(ErrorCode.unknown);
} on DioError catch (err) {
throw err.error!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.storagePathUpdateFailed,
),
);
}
}
}

View File

@@ -1,7 +1,8 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/models/saved_view_model.dart';
import 'package:paperless_api/src/request_utils.dart';
@@ -30,32 +31,28 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
final response = await _client.post(
"/api/saved_views/",
data: view.toJson(),
options: Options(validateStatus: (status) => status == 201),
);
if (response.statusCode == HttpStatus.created) {
return SavedView.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.createSavedViewError,
httpStatusCode: response.statusCode,
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.createSavedViewError),
);
} on DioError catch (err) {
throw err.error!;
}
}
@override
Future<int> delete(SavedView view) async {
try {
final response = await _client.delete("/api/saved_views/${view.id}/");
if (response.statusCode == HttpStatus.noContent) {
return view.id!;
}
throw PaperlessServerException(
ErrorCode.deleteSavedViewError,
httpStatusCode: response.statusCode,
await _client.delete(
"/api/saved_views/${view.id}/",
options: Options(validateStatus: (status) => status == 204),
);
return view.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.deleteSavedViewError),
);
} on DioError catch (err) {
throw err.error!;
}
}

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/models/paperless_server_information_model.dart';
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
@@ -18,8 +19,11 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
@override
Future<PaperlessServerInformationModel> getServerInformation() async {
final response = await client.get("/api/remote_version/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/remote_version/",
options: Options(validateStatus: (status) => status == 200),
);
final version = response.data["version"] as String;
final updateAvailable = response.data["update_available"] as bool;
return PaperlessServerInformationModel(
@@ -27,25 +31,44 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
version: version,
isUpdateAvailable: updateAvailable,
);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.serverInformationLoadFailed,
),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<PaperlessServerStatisticsModel> getServerStatistics() async {
final response = await client.get('/api/statistics/');
if (response.statusCode == 200) {
try {
final response = await client.get(
'/api/statistics/',
options: Options(validateStatus: (status) => status == 200),
);
return PaperlessServerStatisticsModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.serverStatisticsLoadFailed,
),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<PaperlessUiSettingsModel> getUiSettings() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return PaperlessUiSettingsModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.uiSettingsLoadFailed),
);
}
throw const PaperlessServerException.unknown();
}
}

View File

@@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessTasksApiImpl implements PaperlessTasksApi {
final Dio _client;
@@ -41,11 +43,17 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
@override
Future<Iterable<Task>> findAll([Iterable<int>? ids]) async {
final response = await _client.get("/api/tasks/");
if (response.statusCode == 200) {
try {
final response = await _client.get(
"/api/tasks/",
options: Options(validateStatus: (status) => status == 200),
);
return (response.data as List).map((e) => Task.fromJson(e));
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.loadTasksError),
);
}
return [];
}
@override
@@ -74,15 +82,22 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
@override
Future<Iterable<Task>> acknowledgeTasks(Iterable<Task> tasks) async {
final response = await _client.post("/api/acknowledge_tasks/", data: {
try {
final response = await _client.post(
"/api/acknowledge_tasks/",
data: {
'tasks': tasks.map((e) => e.id).toList(),
});
if (response.statusCode == 200) {
},
options: Options(validateStatus: (status) => status == 200),
);
if (response.data['result'] != tasks.length) {
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
throw const PaperlessApiException(ErrorCode.acknowledgeTasksError);
}
return tasks.map((e) => e.copyWith(acknowledged: true)).toList();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.acknowledgeTasksError),
);
}
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
}
}

View File

@@ -1,5 +1,7 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessUserApiV2Impl implements PaperlessUserApi {
final Dio client;
@@ -8,19 +10,33 @@ class PaperlessUserApiV2Impl implements PaperlessUserApi {
@override
Future<int> findCurrentUserId() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/ui_settings/",
options: Options(
validateStatus: (status) => status == 200,
),
);
return response.data['user_id'];
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<UserModel> findCurrentUser() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return UserModelV2.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
}

View File

@@ -1,5 +1,7 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
final Dio dio;
@@ -8,11 +10,17 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
@override
Future<UserModelV3> find(int id) async {
final response = await dio.get("/api/users/$id/");
if (response.statusCode == 200) {
try {
final response = await dio.get(
"/api/users/$id/",
options: Options(validateStatus: (status) => status == 200),
);
return UserModelV3.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
@@ -22,40 +30,59 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
String contains = '',
String username = '',
}) async {
final response = await dio.get("/api/users/", queryParameters: {
try {
final response = await dio.get(
"/api/users/",
queryParameters: {
"username__istartswith": startsWith,
"username__iendswith": endsWith,
"username__icontains": contains,
"username__iexact": username,
});
if (response.statusCode == 200) {
},
options: Options(validateStatus: (status) => status == 200),
);
return PagedSearchResult<UserModelV3>.fromJson(
response.data,
UserModelV3.fromJson as UserModelV3 Function(Object?),
).results;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<int> findCurrentUserId() async {
final response = await dio.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await dio.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return response.data['user']['id'];
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<Iterable<UserModelV3>> findAll() async {
final response = await dio.get("/api/users/");
if (response.statusCode == 200) {
try {
final response = await dio.get(
"/api/users/",
options: Options(validateStatus: (status) => status == 200),
);
return PagedSearchResult<UserModelV3>.fromJson(
response.data,
(json) => UserModelV3.fromJson(json as dynamic),
).results;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override

View File

@@ -2,7 +2,8 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
Future<T?> getSingleResult<T>(
String url,
@@ -16,20 +17,15 @@ Future<T?> getSingleResult<T>(
url,
options: Options(
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
validateStatus: (status) => status == 200,
),
);
if (response.statusCode == HttpStatus.ok) {
return compute(
fromJson,
response.data as Map<String, dynamic>,
);
}
throw PaperlessServerException(
errorCode,
httpStatusCode: response.statusCode,
);
} on DioError catch (err) {
throw err.error!;
} on DioException catch (exception) {
throw exception.unravel(orElse: PaperlessApiException(errorCode));
}
}
@@ -43,30 +39,25 @@ Future<List<T>> getCollection<T>(
try {
final response = await client.get(
url,
options: Options(headers: {
'accept': 'application/json; version=$minRequiredApiVersion'
}),
options: Options(
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
validateStatus: (status) => status == 200,
),
);
if (response.statusCode == HttpStatus.ok) {
final Map<String, dynamic> body = response.data;
if (body.containsKey('count')) {
if (body['count'] == 0) {
return <T>[];
} else {
return compute(
_collectionFromJson,
_CollectionFromJsonSerializationParams(fromJson,
(body['results'] as List).cast<Map<String, dynamic>>()),
_CollectionFromJsonSerializationParams(
fromJson,
(body['results'] as List).cast<Map<String, dynamic>>(),
),
);
}
}
}
throw PaperlessServerException(
errorCode,
httpStatusCode: response.statusCode,
);
} on DioError catch (err) {
throw err.error!;
} on DioException catch (exception) {
throw exception.unravel(orElse: PaperlessApiException(errorCode));
}
}

View File

@@ -29,42 +29,42 @@ packages:
dependency: "direct main"
description:
name: camera
sha256: "309b823e61f15ff6b5b2e4c0ff2e1512ea661cad5355f71fc581e510ae5b26bb"
sha256: ebebead3d5ec3d148249331d751d462d7e8c98102b8830a9b45ec96a2bd4333f
url: "https://pub.dev"
source: hosted
version: "0.10.5"
version: "0.10.5+2"
camera_android:
dependency: transitive
description:
name: camera_android
sha256: e0f9b7eea2d1f4d4f5460f178522f0d02c095d2ae00b01a77419ce61c4184bfe
sha256: f43d07f9d7228ea1ca87d22e30881bd68da4b78484a1fbd1f1408b412a41cefb
url: "https://pub.dev"
source: hosted
version: "0.10.7"
version: "0.10.8+3"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
sha256: "7ac8b950672716722af235eed7a7c37896853669800b7da706bb0a9fd41d3737"
sha256: "1a416e452b30955b392f4efbf23291d3f2ba3660a85e1628859eb62d2a2bab26"
url: "https://pub.dev"
source: hosted
version: "0.9.13+1"
version: "0.9.13+2"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
sha256: "525017018d116c5db8c4c43ec2d9b1663216b369c9f75149158280168a7ce472"
sha256: "60fa0bb62a4f3bf3a7c413e31e4cd01b69c779ccc8e4668904a24581b86c316b"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.5.1"
camera_web:
dependency: transitive
description:
name: camera_web
sha256: d77965f32479ee6d8f48205dcf10f845d7210595c6c00faa51eab265d1cae993
sha256: bcbd775fb3a9d51cc3ece899d54ad66f6306410556bac5759f78e13f9228841f
url: "https://pub.dev"
source: hosted
version: "0.3.1+3"
version: "0.3.1+4"
camerawesome:
dependency: transitive
description:
@@ -178,18 +178,18 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.0.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
url: "https://pub.dev"
source: hosted
version: "2.0.14"
version: "2.0.15"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -220,10 +220,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015"
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
matcher:
dependency: transitive
description:
@@ -299,10 +299,10 @@ packages:
dependency: transitive
description:
name: path_provider_linux
sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
url: "https://pub.dev"
source: hosted
version: "2.1.10"
version: "2.1.11"
path_provider_platform_interface:
dependency: transitive
description:
@@ -315,10 +315,10 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
url: "https://pub.dev"
source: hosted
version: "2.1.6"
version: "2.1.7"
petitparser:
dependency: transitive
description:
@@ -456,10 +456,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee
url: "https://pub.dev"
source: hosted
version: "4.1.4"
version: "5.0.5"
xdg_directories:
dependency: transitive
description:
@@ -477,5 +477,5 @@ packages:
source: hosted
version: "6.3.0"
sdks:
dart: ">=3.0.0-417 <4.0.0"
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.3.0"

View File

@@ -101,10 +101,10 @@ packages:
dependency: transitive
description:
name: bidi
sha256: dc00274c7edabae2ab30c676e736ea1eb0b1b7a1b436cb5fe372e431ccb39ab0
sha256: "6794b226bc939731308b8539c49bb6c2fdbf0e78c3a65e9b9e81e727c256dfe6"
url: "https://pub.dev"
source: hosted
version: "2.0.6"
version: "2.0.7"
bloc:
dependency: transitive
description:
@@ -133,10 +133,10 @@ packages:
dependency: transitive
description:
name: build
sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc"
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
build_config:
dependency: transitive
description:
@@ -157,18 +157,18 @@ packages:
dependency: transitive
description:
name: build_resolvers
sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95
sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "5e1929ad37d48bd382b124266cb8e521de5548d406a45a5ae6656c13dab73e37"
sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b"
url: "https://pub.dev"
source: hosted
version: "2.4.5"
version: "2.4.6"
build_runner_core:
dependency: transitive
description:
@@ -333,10 +333,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
dbus:
dependency: transitive
description:
@@ -389,27 +389,27 @@ packages:
dependency: transitive
description:
name: dots_indicator
sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c
sha256: "58b6a365744aa62aa1b70c4ea29e5106fbe064f5edaf7e9652e9b856edbfd9bb"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "3.0.0"
dynamic_color:
dependency: "direct main"
description:
name: dynamic_color
sha256: "74dff1435a695887ca64899b8990004f8d1232b0e84bfc4faa1fdda7c6f57cc1"
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d
url: "https://pub.dev"
source: hosted
version: "1.6.5"
version: "1.6.6"
edge_detection:
dependency: "direct main"
description:
path: "."
ref: master
resolved-ref: "6ca5e015fc9cb4603890bddacdea0cafb839650d"
resolved-ref: "01636d9050d409177934ec64876c1c83c2567513"
url: "https://github.com/sawankumarbundelkhandi/edge_detection"
source: git
version: "1.1.1"
version: "1.1.2"
equatable:
dependency: "direct main"
description:
@@ -483,10 +483,10 @@ packages:
dependency: "direct main"
description:
name: flutter_cache_manager
sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3"
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "3.3.1"
flutter_colorpicker:
dependency: "direct main"
description:
@@ -698,10 +698,10 @@ packages:
dependency: "direct main"
description:
name: flutter_typeahead
sha256: d72e7079d01b4ec109a12b01b06a85c09a41ae4531f8a0ca5ef9f759ce4e64a2
sha256: a3539f7a90246b152f569029dedcf0b842532d3f2a440701b520e0bf2acbcf42
url: "https://pub.dev"
source: hosted
version: "4.6.1"
version: "4.6.2"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -719,10 +719,10 @@ packages:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206"
sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3"
url: "https://pub.dev"
source: hosted
version: "10.4.0"
version: "10.5.0"
freezed:
dependency: "direct dev"
description:
@@ -764,10 +764,10 @@ packages:
dependency: transitive
description:
name: graphs
sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.1"
hive:
dependency: "direct main"
description:
@@ -804,10 +804,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "4c3f04bfb64d3efd508d06b41b825542f08122d30bda4933fb95c069d22a4fa3"
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
http_methods:
dependency: transitive
description:
@@ -836,10 +836,10 @@ packages:
dependency: "direct main"
description:
name: hydrated_bloc
sha256: "0ea117b32259d9a79c2a2d33eef92e9dd676b88ec4f1ef94102c5889ca1673b6"
sha256: "24994e61f64904d911683cce1a31dc4ef611619da5253f1de2b7b8fc6f79a118"
url: "https://pub.dev"
source: hosted
version: "9.1.1"
version: "9.1.2"
image:
dependency: "direct main"
description:
@@ -865,10 +865,10 @@ packages:
dependency: "direct main"
description:
name: introduction_screen
sha256: f194ae655a84b945a2aedb7961d09948d789fc91088efb032666112923bcbc1e
sha256: f39be426026785b8fea4ed93e226e7fc28ef49a4c78c3f86c958bae26dabef00
url: "https://pub.dev"
source: hosted
version: "3.1.8"
version: "3.1.9"
io:
dependency: transitive
description:
@@ -905,10 +905,10 @@ packages:
dependency: "direct dev"
description:
name: json_serializable
sha256: "61a60716544392a82726dd0fa1dd6f5f1fd32aec66422b6e229e7b90d52325c4"
sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.7.1"
lints:
dependency: transitive
description:
@@ -961,10 +961,10 @@ packages:
dependency: transitive
description:
name: local_auth_windows
sha256: "19323b75ab781d5362dbb15dcb7e0916d2431c7a6dbdda016ec9708689877f73"
sha256: "5af808e108c445d0cf702a8c5f8242f1363b7970320334f82e6e1e8ad0b0d7d4"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
version: "1.0.9"
logging:
dependency: transitive
description:
@@ -1179,54 +1179,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.10.4"
pedantic:
dependency: transitive
description:
name: pedantic
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "1b6b3e73f0bcbc856548bbdfb1c33084a401c4f143e220629a9055233d76c331"
sha256: "415af30ba76a84faccfe1eb251fe1e4fdc790f876924c65ad7d6ed7a1404bcd6"
url: "https://pub.dev"
source: hosted
version: "10.3.0"
version: "10.4.2"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "8f6a95ccbca13766882f95d32684d7c9bfe6c45650c32bedba948ef1c6a4ddf7"
sha256: "3b61f3da3b1c83bc3fb6a2b431e8dab01d0e5b45f6a3d9c7609770ec88b2a89e"
url: "https://pub.dev"
source: hosted
version: "10.2.3"
version: "10.3.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "08dcb6ce628ac0b257e429944b4c652c2a4e6af725bdf12b498daa2c6b2b1edb"
sha256: "7a187b671a39919462af2b5e813148365b71a615979165a119868d667fe90c03"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
version: "9.1.3"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: de20a5c3269229c1ae2e5a6b822f6cb59578b23e8255c93fbeebfc82116e6b11
sha256: "463a07cb7cc6c758a7a1c7da36ce666bb80a0b4b5e92df0fa36872e0ed456993"
url: "https://pub.dev"
source: hosted
version: "3.10.0"
version: "3.11.1"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
url: "https://pub.dev"
source: hosted
version: "0.1.2"
version: "0.1.3"
petitparser:
dependency: transitive
description:
@@ -1327,10 +1319,10 @@ packages:
dependency: transitive
description:
name: pub_updater
sha256: "05ae70703e06f7fdeb05f7f02dd680b8aad810e87c756a618f33e1794635115c"
sha256: b06600619c8c219065a548f8f7c192b3e080beff95488ed692780f48f69c0625
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.3.1"
pubspec_parse:
dependency: transitive
description:
@@ -1484,18 +1476,18 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33"
sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.4.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f"
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
version: "1.3.4"
source_map_stack_trace:
dependency: transitive
description:
@@ -1652,18 +1644,18 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
url: "https://pub.dev"
source: hosted
version: "6.1.11"
version: "6.1.12"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51
sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03"
url: "https://pub.dev"
source: hosted
version: "6.0.35"
version: "6.0.36"
url_launcher_ios:
dependency: transitive
description:
@@ -1692,26 +1684,26 @@ packages:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab"
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
url: "https://pub.dev"
source: hosted
version: "2.0.17"
version: "2.0.18"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771"
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.0.7"
uuid:
dependency: "direct main"
description:
@@ -1780,10 +1772,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: "57a22c86065375c1598b57224f92d6008141be0c877c64100de8bfb6f71083d8"
sha256: "1c93e96f3069bacdc734fad6b7e1d3a480fd516a3ae5b8858becf7f07515a2f3"
url: "https://pub.dev"
source: hosted
version: "3.7.1"
version: "3.8.2"
webview_flutter_platform_interface:
dependency: transitive
description:
@@ -1796,10 +1788,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "6bbc6ade302b842999b27cbaa7171241c273deea8a9c73f92ceb3d811c767de2"
sha256: a8d7e8b4be2a79e83b70235369971ec97d14df4cdbb40d305a8eeae67d8e6432
url: "https://pub.dev"
source: hosted
version: "3.4.4"
version: "3.6.2"
win32:
dependency: transitive
description:

View File

@@ -2,9 +2,9 @@ import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/core/type/types.dart';
Future<T> loadOne<T>(String filePath, T Function(JSON) transformFn, int? id) async {
Future<T> loadOne<T>(String filePath,
T Function(Map<String, dynamic>) transformFn, int? id) async {
if (id != null) {
final coll = await loadCollection(filePath, transformFn);
return coll.firstWhere((dynamic element) => element.id == id);
@@ -13,22 +13,27 @@ Future<T> loadOne<T>(String filePath, T Function(JSON) transformFn, int? id) asy
return transformFn(jsonDecode(response));
}
Future<List<T>> loadCollection<T>(String filePath, T Function(JSON) transformFn,
Future<List<T>> loadCollection<T>(
String filePath, T Function(Map<String, dynamic>) transformFn,
{int? numItems, List<int>? ids}) async {
assert(((numItems != null) ^ (ids != null)) || (numItems == null && ids == null));
assert(((numItems != null) ^ (ids != null)) ||
(numItems == null && ids == null));
final String response = await rootBundle.loadString(filePath);
final lst = (jsonDecode(response) as List<dynamic>);
final res = (jsonDecode(response) as List<dynamic>).map((e) => transformFn(e)).toList();
final res = (jsonDecode(response) as List<dynamic>)
.map((e) => transformFn(e))
.toList();
if (ids != null) {
return res.where((dynamic element) => ids.contains(element.id)).toList();
}
if (numItems != null && lst.length < numItems) {
throw Exception("The requested collection contains only ${lst.length} items!");
throw Exception(
"The requested collection contains only ${lst.length} items!");
} else {
return res.sublist(0, numItems);
}
}
const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
String getRandomString(int length) => String.fromCharCodes(
Iterable.generate(length, (_) => _chars.codeUnitAt(Random().nextInt(_chars.length))));
String getRandomString(int length) => String.fromCharCodes(Iterable.generate(
length, (_) => _chars.codeUnitAt(Random().nextInt(_chars.length))));