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:dio/dio.dart';
import 'package:paperless_api/paperless_api.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 { class DioHttpErrorInterceptor extends Interceptor {
@override @override
void onError(DioError err, ErrorInterceptorHandler handler) { void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 400) { if (err.response?.statusCode == 400) {
// try to parse contained error message, otherwise return response final data = err.response!.data;
final dynamic data = err.response?.data; if (PaperlessServerMessageException.canParse(data)) {
if (data is Map<String, dynamic>) { final exception = PaperlessServerMessageException.fromJson(data);
return _handlePaperlessValidationError(data, handler, err); final message = exception.detail;
} 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")) {
handler.reject( handler.reject(
DioError( DioException(
message: data['detail'], message: message,
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
error: ServerMessageException(data['detail']), error: exception,
response: err.response, response: err.response,
type: DioErrorType.unknown, type: DioExceptionType.badResponse,
), ),
); );
return; } else if (PaperlessFormValidationException.canParse(data)) {
} final exception = PaperlessFormValidationException.fromJson(data);
} else if (err.error is SocketException) { handler.reject(
final ex = err.error as SocketException; DioException(
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),
requestOptions: err.requestOptions, 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),
), ),
); );
} }
} } else {
return handler.reject(err); return handler.next(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),
),
);
} }
} }
} }
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 @override
void onError(DioError err, ErrorInterceptorHandler handler) async { void onError(DioException err, ErrorInterceptorHandler handler) async {
if (_shouldRetryOnHttpException(err)) { if (_shouldRetryOnHttpException(err)) {
try { try {
handler.resolve(await DioHttpRequestRetrier(dio: dio) handler.resolve(await DioHttpRequestRetrier(dio: dio)
@@ -27,8 +27,8 @@ class RetryOnConnectionChangeInterceptor extends Interceptor {
} }
} }
bool _shouldRetryOnHttpException(DioError err) { bool _shouldRetryOnHttpException(DioException err) {
return err.type == DioErrorType.unknown && return err.type == DioExceptionType.unknown &&
(err.error is HttpException && (err.error is HttpException &&
(err.message?.contains( (err.message?.contains(
'Connection closed before full header was received', '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"; static const _missingClientCertText = "No required SSL certificate was sent";
@override @override
void onError(DioError err, ErrorInterceptorHandler handler) { void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 400) { if (err.response?.statusCode == 400) {
final message = err.response?.data; final message = err.response?.data;
if (message is String && message.contains(_missingClientCertText)) { 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( return _rejectWithStatus(
ReachabilityStatus.connectionTimeout, ReachabilityStatus.connectionTimeout,
err, err,
@@ -48,13 +48,13 @@ class ServerReachabilityErrorInterceptor extends Interceptor {
void _rejectWithStatus( void _rejectWithStatus(
ReachabilityStatus reachabilityStatus, ReachabilityStatus reachabilityStatus,
DioError err, DioException err,
ErrorInterceptorHandler handler, ErrorInterceptorHandler handler,
) { ) {
handler.reject(DioError( handler.reject(DioException(
error: reachabilityStatus, error: reachabilityStatus,
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
response: err.response, response: err.response,
type: DioErrorType.unknown, type: DioExceptionType.unknown,
)); ));
} }

View File

@@ -354,6 +354,7 @@ Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
final labelRepo = context.read<LabelRepository>(); final labelRepo = context.read<LabelRepository>();
final docsApi = context.read<PaperlessDocumentsApi>(); final docsApi = context.read<PaperlessDocumentsApi>();
final connectivity = context.read<Connectivity>(); final connectivity = context.read<Connectivity>();
final apiVersion = context.read<ApiVersion>();
return Navigator.of(context).push<DocumentUploadResult>( return Navigator.of(context).push<DocumentUploadResult>(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MultiProvider( builder: (_) => MultiProvider(
@@ -361,6 +362,7 @@ Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
Provider.value(value: labelRepo), Provider.value(value: labelRepo),
Provider.value(value: docsApi), Provider.value(value: docsApi),
Provider.value(value: connectivity), Provider.value(value: connectivity),
Provider.value(value: apiVersion)
], ],
builder: (_, child) => BlocProvider( builder: (_, child) => BlocProvider(
create: (_) => DocumentUploadCubit( create: (_) => DocumentUploadCubit(

View File

@@ -88,7 +88,6 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
if (correspondent != null) { if (correspondent != null) {
final updatedState = {...state.correspondents}..[id] = correspondent; final updatedState = {...state.correspondents}..[id] = correspondent;
emit(state.copyWith(correspondents: updatedState)); emit(state.copyWith(correspondents: updatedState));
return correspondent; return correspondent;
} }
return null; return null;

View File

@@ -4,13 +4,14 @@ import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.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/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart';
/// Manages the security context, authentication and base request URL for /// Manages the security context, authentication and base request URL for
/// an underlying [Dio] client which is injected into all services /// 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> { class SessionManager extends ValueNotifier<Dio> {
Dio get client => value; Dio get client => value;
@@ -20,16 +21,21 @@ class SessionManager extends ValueNotifier<Dio> {
static Dio _initDio(List<Interceptor> interceptors) { static Dio _initDio(List<Interceptor> interceptors) {
//en- and decoded by utf8 by default //en- and decoded by utf8 by default
final Dio dio = Dio( final Dio dio = Dio(
BaseOptions(contentType: Headers.jsonContentType), BaseOptions(
contentType: Headers.jsonContentType,
followRedirects: true,
maxRedirects: 10,
),
); );
dio.options dio.options
..receiveTimeout = const Duration(seconds: 30) ..receiveTimeout = const Duration(seconds: 30)
..sendTimeout = const Duration(seconds: 60) ..sendTimeout = const Duration(seconds: 60)
..responseType = ResponseType.json; ..responseType = ResponseType.json;
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient =
(client) => client..badCertificateCallback = (cert, host, port) => true; () => HttpClient()..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll([ dio.interceptors.addAll([
...interceptors, ...interceptors,
DioUnauthorizedInterceptor(),
DioHttpErrorInterceptor(), DioHttpErrorInterceptor(),
PrettyDioLogger( PrettyDioLogger(
compact: true, compact: true,
@@ -64,7 +70,7 @@ class SessionManager extends ValueNotifier<Dio> {
password: clientCertificate.passphrase, password: clientCertificate.passphrase,
); );
final adapter = IOHttpClientAdapter() final adapter = IOHttpClientAdapter()
..onHttpClientCreate = (client) => HttpClient(context: context) ..createHttpClient = () => HttpClient(context: context)
..badCertificateCallback = ..badCertificateCallback =
(X509Certificate cert, String host, int port) => true; (X509Certificate cert, String host, int port) => true;

View File

@@ -83,8 +83,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
return ReachabilityStatus.reachable; return ReachabilityStatus.reachable;
} }
return ReachabilityStatus.notReachable; return ReachabilityStatus.notReachable;
} on DioError catch (error) { } on DioException catch (error) {
if (error.type == DioErrorType.unknown && if (error.type == DioExceptionType.unknown &&
error.error is ReachabilityStatus) { error.error is ReachabilityStatus) {
return error.error as ReachabilityStatus; return error.error as ReachabilityStatus;
} }

View File

@@ -12,7 +12,7 @@ class FileService {
) async { ) async {
final dir = await documentsDirectory; final dir = await documentsDirectory;
if (dir == null) { if (dir == null) {
throw const PaperlessServerException.unknown(); //TODO: better handling throw const PaperlessApiException.unknown(); //TODO: better handling
} }
File file = File("${dir.path}/$filename"); File file = File("${dir.path}/$filename");
return file..writeAsBytes(bytes); 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'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
String translateError(BuildContext context, ErrorCode code) { String translateError(BuildContext context, ErrorCode code) {
switch (code) { return switch (code) {
case ErrorCode.unknown: ErrorCode.unknown => S.of(context)!.anUnknownErrorOccurred,
return S.of(context)!.anUnknownErrorOccurred; ErrorCode.authenticationFailed =>
case ErrorCode.authenticationFailed: S.of(context)!.authenticationFailedPleaseTryAgain,
return S.of(context)!.authenticationFailedPleaseTryAgain; ErrorCode.notAuthenticated => S.of(context)!.userIsNotAuthenticated,
case ErrorCode.notAuthenticated: ErrorCode.documentUploadFailed => S.of(context)!.couldNotUploadDocument,
return S.of(context)!.userIsNotAuthenticated; ErrorCode.documentUpdateFailed => S.of(context)!.couldNotUpdateDocument,
case ErrorCode.documentUploadFailed: ErrorCode.documentLoadFailed => S.of(context)!.couldNotLoadDocuments,
return S.of(context)!.couldNotUploadDocument; ErrorCode.documentDeleteFailed => S.of(context)!.couldNotDeleteDocument,
case ErrorCode.documentUpdateFailed: ErrorCode.documentPreviewFailed =>
return S.of(context)!.couldNotUpdateDocument; S.of(context)!.couldNotLoadDocumentPreview,
case ErrorCode.documentLoadFailed: ErrorCode.documentAsnQueryFailed =>
return S.of(context)!.couldNotLoadDocuments; S.of(context)!.couldNotAssignArchiveSerialNumber,
case ErrorCode.documentDeleteFailed: ErrorCode.tagCreateFailed => S.of(context)!.couldNotCreateTag,
return S.of(context)!.couldNotDeleteDocument; ErrorCode.tagLoadFailed => S.of(context)!.couldNotLoadTags,
case ErrorCode.documentPreviewFailed: ErrorCode.documentTypeCreateFailed => S.of(context)!.couldNotCreateDocument,
return S.of(context)!.couldNotLoadDocumentPreview; ErrorCode.documentTypeLoadFailed =>
case ErrorCode.documentAsnQueryFailed: S.of(context)!.couldNotLoadDocumentTypes,
return S.of(context)!.couldNotAssignArchiveSerialNumber; ErrorCode.correspondentCreateFailed =>
case ErrorCode.tagCreateFailed: S.of(context)!.couldNotCreateCorrespondent,
return S.of(context)!.couldNotCreateTag; ErrorCode.correspondentLoadFailed =>
case ErrorCode.tagLoadFailed: S.of(context)!.couldNotLoadCorrespondents,
return S.of(context)!.couldNotLoadTags; ErrorCode.scanRemoveFailed =>
case ErrorCode.documentTypeCreateFailed: S.of(context)!.anErrorOccurredRemovingTheScans,
return S.of(context)!.couldNotCreateDocument; ErrorCode.invalidClientCertificateConfiguration =>
case ErrorCode.documentTypeLoadFailed: S.of(context)!.invalidCertificateOrMissingPassphrase,
return S.of(context)!.couldNotLoadDocumentTypes; ErrorCode.documentBulkActionFailed =>
case ErrorCode.correspondentCreateFailed: S.of(context)!.couldNotBulkEditDocuments,
return S.of(context)!.couldNotCreateCorrespondent; ErrorCode.biometricsNotSupported =>
case ErrorCode.correspondentLoadFailed: S.of(context)!.biometricAuthenticationNotSupported,
return S.of(context)!.couldNotLoadCorrespondents; ErrorCode.biometricAuthenticationFailed =>
case ErrorCode.scanRemoveFailed: S.of(context)!.biometricAuthenticationFailed,
return S.of(context)!.anErrorOccurredRemovingTheScans; ErrorCode.deviceOffline => S.of(context)!.youAreCurrentlyOffline,
case ErrorCode.invalidClientCertificateConfiguration: ErrorCode.serverUnreachable =>
return S.of(context)!.invalidCertificateOrMissingPassphrase; S.of(context)!.couldNotReachYourPaperlessServer,
case ErrorCode.documentBulkActionFailed: ErrorCode.similarQueryError => S.of(context)!.couldNotLoadSimilarDocuments,
return S.of(context)!.couldNotBulkEditDocuments; ErrorCode.autocompleteQueryError =>
case ErrorCode.biometricsNotSupported: S.of(context)!.anErrorOccurredWhileTryingToAutocompleteYourQuery,
return S.of(context)!.biometricAuthenticationNotSupported; ErrorCode.storagePathLoadFailed => S.of(context)!.couldNotLoadStoragePaths,
case ErrorCode.biometricAuthenticationFailed: ErrorCode.storagePathCreateFailed =>
return S.of(context)!.biometricAuthenticationFailed; S.of(context)!.couldNotCreateStoragePath,
case ErrorCode.deviceOffline: ErrorCode.loadSavedViewsError => S.of(context)!.couldNotLoadSavedViews,
return S.of(context)!.youAreCurrentlyOffline; ErrorCode.createSavedViewError => S.of(context)!.couldNotCreateSavedView,
case ErrorCode.serverUnreachable: ErrorCode.deleteSavedViewError => S.of(context)!.couldNotDeleteSavedView,
return S.of(context)!.couldNotReachYourPaperlessServer; ErrorCode.requestTimedOut => S.of(context)!.requestTimedOut,
case ErrorCode.similarQueryError: ErrorCode.unsupportedFileFormat => S.of(context)!.fileFormatNotSupported,
return S.of(context)!.couldNotLoadSimilarDocuments; ErrorCode.missingClientCertificate =>
case ErrorCode.autocompleteQueryError: S.of(context)!.aClientCertificateWasExpectedButNotSent,
return S.of(context)!.anErrorOccurredWhileTryingToAutocompleteYourQuery; ErrorCode.suggestionsQueryError => S.of(context)!.couldNotLoadSuggestions,
case ErrorCode.storagePathLoadFailed: ErrorCode.acknowledgeTasksError => S.of(context)!.couldNotAcknowledgeTasks,
return S.of(context)!.couldNotLoadStoragePaths; ErrorCode.correspondentDeleteFailed =>
case ErrorCode.storagePathCreateFailed: "Could not delete correspondent, please try again.",
return S.of(context)!.couldNotCreateStoragePath; ErrorCode.documentTypeDeleteFailed =>
case ErrorCode.loadSavedViewsError: "Could not delete document type, please try again.",
return S.of(context)!.couldNotLoadSavedViews; ErrorCode.tagDeleteFailed => "Could not delete tag, please try again.",
case ErrorCode.createSavedViewError: ErrorCode.correspondentUpdateFailed =>
return S.of(context)!.couldNotCreateSavedView; "Could not update correspondent, please try again.",
case ErrorCode.deleteSavedViewError: ErrorCode.documentTypeUpdateFailed =>
return S.of(context)!.couldNotDeleteSavedView; "Could not update document type, please try again.",
case ErrorCode.requestTimedOut: ErrorCode.tagUpdateFailed => "Could not update tag, please try again.",
return S.of(context)!.requestTimedOut; ErrorCode.storagePathDeleteFailed =>
case ErrorCode.unsupportedFileFormat: "Could not delete storage path, please try again.",
return S.of(context)!.fileFormatNotSupported; ErrorCode.storagePathUpdateFailed =>
case ErrorCode.missingClientCertificate: "Could not update storage path, please try again.",
return S.of(context)!.aClientCertificateWasExpectedButNotSent; ErrorCode.serverInformationLoadFailed =>
case ErrorCode.suggestionsQueryError: "Could not load server information.",
return S.of(context)!.couldNotLoadSuggestions; ErrorCode.serverStatisticsLoadFailed => "Could not load server statistics.",
case ErrorCode.acknowledgeTasksError: ErrorCode.uiSettingsLoadFailed => "Could not load UI settings",
return S.of(context)!.couldNotAcknowledgeTasks; 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 { Future<void> loadFullContent() async {
final doc = await _api.find(state.document.id); final doc = await _api.find(state.document.id);
if (doc == null) { emit(
return; state.copyWith(
} isFullContentLoaded: true,
emit(state.copyWith( fullContent: doc.content,
isFullContentLoaded: true, ),
fullContent: doc.content, );
));
} }
Future<void> assignAsn( Future<void> assignAsn(
@@ -99,13 +98,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
Future<ResultType> openDocumentInSystemViewer() async { Future<ResultType> openDocumentInSystemViewer() async {
final cacheDir = await FileService.temporaryDirectory; final cacheDir = await FileService.temporaryDirectory;
//TODO: Why is this cleared here?
await FileService.clearDirectoryContent(PaperlessDirectoryType.temporary);
if (state.metaData == null) { if (state.metaData == null) {
await loadMetaData(); await loadMetaData();
} }
final desc = FileDescription.fromPath( final desc = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " ")); state.metaData!.mediaFilename.replaceAll("/", " "),
);
final fileName = "${desc.filename}.pdf"; final fileName = "${desc.filename}.pdf";
final file = File("${cacheDir.path}/$fileName"); final file = File("${cacheDir.path}/$fileName");

View File

@@ -287,8 +287,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Widget _buildEditButton() { Widget _buildEditButton() {
bool canEdit = context.watchInternetConnection && bool canEdit = context.watchInternetConnection &&
LocalUserAccount.current.paperlessUser LocalUserAccount.current.paperlessUser.canEditDocuments;
.hasPermission(PermissionAction.change, PermissionTarget.document);
if (!canEdit) { if (!canEdit) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -319,8 +318,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final isConnected = connectivityState.isConnected; final isConnected = connectivityState.isConnected;
final canDelete = isConnected && final canDelete = isConnected &&
LocalUserAccount.current.paperlessUser.hasPermission( LocalUserAccount.current.paperlessUser.canDeleteDocuments;
PermissionAction.delete, PermissionTarget.document);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
@@ -430,7 +428,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
try { try {
await context.read<DocumentDetailsCubit>().delete(document); await context.read<DocumentDetailsCubit>().delete(document);
showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted); showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} finally { } finally {
// Document deleted => go back to primary route // 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_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -48,10 +47,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userCanEditDocument = final userCanEditDocument =
LocalUserAccount.current.paperlessUser.hasPermission( LocalUserAccount.current.paperlessUser.canEditDocuments;
PermissionAction.change,
PermissionTarget.document,
);
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>( return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous.document.archiveSerialNumber != previous.document.archiveSerialNumber !=
@@ -124,12 +120,14 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
.read<DocumentDetailsCubit>() .read<DocumentDetailsCubit>()
.assignAsn(widget.document, asn: asn) .assignAsn(widget.document, asn: asn)
.then((value) => _onAsnUpdated()) .then((value) => _onAsnUpdated())
.onError<PaperlessServerException>( .onError<PaperlessApiException>(
(error, stackTrace) => showErrorMessage(context, error, stackTrace), (error, stackTrace) => showErrorMessage(context, error, stackTrace),
) )
.onError<PaperlessValidationErrors>( .onError<PaperlessFormValidationException>(
(error, stackTrace) => setState(() => _errors = error), (error, stackTrace) {
); setState(() => _errors = error.validationMessages);
},
);
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
} }
@@ -141,9 +139,10 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
autoAssign: true, autoAssign: true,
) )
.then((value) => _onAsnUpdated()) .then((value) => _onAsnUpdated())
.onError<PaperlessServerException>( .onError<PaperlessApiException>(
(error, stackTrace) => showErrorMessage(context, error, stackTrace), (error, stackTrace) => showErrorMessage(context, error, stackTrace),
); )
.catchError((error) => showGenericError(context, error));
} }
void _onAsnUpdated() { void _onAsnUpdated() {

View File

@@ -95,7 +95,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
locale: globalSettings.preferredLocaleSubtag, locale: globalSettings.preferredLocaleSubtag,
); );
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded); // showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} catch (error) { } catch (error) {
showGenericError(context, error); showGenericError(context, error);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
@@ -45,38 +46,35 @@ class DocumentOverviewWidget extends StatelessWidget {
context: context, context: context,
label: S.of(context)!.createdAt, label: S.of(context)!.createdAt,
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
Visibility( if (document.documentType != null &&
visible: document.documentType != null, LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
child: DetailsItem( DetailsItem(
label: S.of(context)!.documentType, label: S.of(context)!.documentType,
content: LabelText<DocumentType>( content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
label: availableDocumentTypes[document.documentType], label: availableDocumentTypes[document.documentType],
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
), if (document.correspondent != null &&
Visibility( LocalUserAccount.current.paperlessUser.canViewCorrespondents)
visible: document.correspondent != null, DetailsItem(
child: DetailsItem(
label: S.of(context)!.correspondent, label: S.of(context)!.correspondent,
content: LabelText<Correspondent>( content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
label: availableCorrespondents[document.correspondent], label: availableCorrespondents[document.correspondent],
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
), if (document.storagePath != null &&
Visibility( LocalUserAccount.current.paperlessUser.canViewStoragePaths)
visible: document.storagePath != null, DetailsItem(
child: DetailsItem(
label: S.of(context)!.storagePath, label: S.of(context)!.storagePath,
content: LabelText<StoragePath>( content: LabelText<StoragePath>(
label: availableStoragePaths[document.storagePath], label: availableStoragePaths[document.storagePath],
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
), if (document.tags.isNotEmpty &&
Visibility( LocalUserAccount.current.paperlessUser.canViewTags)
visible: document.tags.isNotEmpty, DetailsItem(
child: DetailsItem(
label: S.of(context)!.tags, label: S.of(context)!.tags,
content: Padding( content: Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
@@ -86,7 +84,6 @@ class DocumentOverviewWidget extends StatelessWidget {
), ),
), ),
).paddedOnly(bottom: itemSpacing), ).paddedOnly(bottom: itemSpacing),
),
], ],
), ),
); );

View File

@@ -90,7 +90,7 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
await context.read<DocumentDetailsCubit>().shareDocument( await context.read<DocumentDetailsCubit>().shareDocument(
shareOriginal: original, shareOriginal: original,
); );
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} catch (error) { } catch (error) {
showGenericError(context, error); showGenericError(context, error);

View File

@@ -123,12 +123,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkCorrespondent, name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined), prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount canCreateNewLabel: LocalUserAccount.current
.current.paperlessUser .paperlessUser.canCreateCorrespondents,
.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
), ),
if (_filteredSuggestions if (_filteredSuggestions
?.hasSuggestedCorrespondents ?? ?.hasSuggestedCorrespondents ??
@@ -164,12 +160,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
initialName: currentInput, initialName: currentInput,
), ),
), ),
canCreateNewLabel: LocalUserAccount canCreateNewLabel: LocalUserAccount.current
.current.paperlessUser .paperlessUser.canCreateDocumentTypes,
.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
addLabelText: S.of(context)!.addDocumentType, addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType, labelText: S.of(context)!.documentType,
initialValue: initialValue:
@@ -214,12 +206,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
child: AddStoragePathPage( child: AddStoragePathPage(
initalName: initialValue), initalName: initialValue),
), ),
canCreateNewLabel: LocalUserAccount canCreateNewLabel: LocalUserAccount.current
.current.paperlessUser .paperlessUser.canCreateStoragePaths,
.hasPermission(
PermissionAction.add,
PermissionTarget.storagePath,
),
addLabelText: S.of(context)!.addStoragePath, addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath, labelText: S.of(context)!.storagePath,
options: state.storagePaths, options: state.storagePaths,
@@ -328,7 +316,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
try { try {
await context.read<DocumentEditCubit>().updateDocument(mergedDocument); await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated); showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} finally { } finally {
setState(() { setState(() {

View File

@@ -22,7 +22,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
scans.removeAt(fileIndex); scans.removeAt(fileIndex);
emit(scans); emit(scans);
} catch (_) { } catch (_) {
throw const PaperlessServerException(ErrorCode.scanRemoveFailed); throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
} }
} }
@@ -37,7 +37,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
imageCache.clear(); imageCache.clear();
emit([]); emit([]);
} catch (_) { } 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) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: searchBarHandle, handle: searchBarHandle,
sliver: const SliverSearchBar(), sliver: SliverSearchBar(
titleText: S.of(context)!.scanner,
),
), ),
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: actionsHandle, handle: actionsHandle,
@@ -322,7 +324,7 @@ class _ScannerPageState extends State<ScannerPage>
onDelete: () async { onDelete: () async {
try { try {
context.read<DocumentScannerCubit>().removeScan(index); context.read<DocumentScannerCubit>().removeScan(index);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
}, },
@@ -339,7 +341,7 @@ class _ScannerPageState extends State<ScannerPage>
void _reset(BuildContext context) { void _reset(BuildContext context) {
try { try {
context.read<DocumentScannerCubit>().reset(); context.read<DocumentScannerCubit>().reset();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }
@@ -360,7 +362,7 @@ class _ScannerPageState extends State<ScannerPage>
)) { )) {
showErrorMessage( showErrorMessage(
context, context,
const PaperlessServerException(ErrorCode.unsupportedFileFormat), const PaperlessApiException(ErrorCode.unsupportedFileFormat),
); );
return; return;
} }

View File

@@ -1,32 +1,74 @@
import 'package:flutter/material.dart'; 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/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 { class SliverSearchBar extends StatelessWidget {
final bool floating; final bool floating;
final bool pinned; final bool pinned;
final String titleText;
const SliverSearchBar({ const SliverSearchBar({
super.key, super.key,
this.floating = false, this.floating = false,
this.pinned = false, this.pinned = false,
required this.titleText,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverPadding( if (LocalUserAccount.current.paperlessUser.canViewDocuments) {
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), return SliverAppBar(
sliver: SliverPersistentHeader( toolbarHeight: kToolbarHeight,
floating: floating, flexibleSpace: Container(
pinned: pinned, margin: const EdgeInsets.symmetric(horizontal: 16.0),
delegate: CustomizableSliverPersistentHeaderDelegate( child: const DocumentSearchBar(),
minExtent: kToolbarHeight,
maxExtent: kToolbarHeight,
child: 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/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/repository/label_repository.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.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_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_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/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:provider/provider.dart';
class DocumentUploadResult { class DocumentUploadResult {
final bool success; final bool success;
@@ -56,7 +57,7 @@ class _DocumentUploadPreparationPageState
final GlobalKey<FormBuilderState> _formKey = GlobalKey(); final GlobalKey<FormBuilderState> _formKey = GlobalKey();
PaperlessValidationErrors _errors = {}; Map<String, String> _errors = {};
bool _isUploadLoading = false; bool _isUploadLoading = false;
late bool _syncTitleAndFilename; late bool _syncTitleAndFilename;
bool _showDatePickerDeleteIcon = false; bool _showDatePickerDeleteIcon = false;
@@ -197,54 +198,64 @@ class _DocumentUploadPreparationPageState
), ),
), ),
// Correspondent // Correspondent
LabelFormField<Correspondent>( if (LocalUserAccount
showAnyAssignedOption: false, .current.paperlessUser.canViewCorrespondents)
showNotAssignedOption: false, LabelFormField<Correspondent>(
addLabelPageBuilder: (initialName) => showAnyAssignedOption: false,
RepositoryProvider.value( showNotAssignedOption: false,
value: context.read<LabelRepository>(), addLabelPageBuilder: (initialName) => MultiProvider(
child: AddCorrespondentPage(initialName: initialName), providers: [
Provider.value(
value: context.read<LabelRepository>(),
),
Provider.value(
value: context.read<ApiVersion>(),
)
],
child: AddCorrespondentPage(initialName: initialName),
),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey,
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateCorrespondents,
), ),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey,
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
),
// Document type // Document type
LabelFormField<DocumentType>( if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
showAnyAssignedOption: false, LabelFormField<DocumentType>(
showNotAssignedOption: false, showAnyAssignedOption: false,
addLabelPageBuilder: (initialName) => showNotAssignedOption: false,
RepositoryProvider.value( addLabelPageBuilder: (initialName) => MultiProvider(
value: context.read<LabelRepository>(), providers: [
child: AddDocumentTypePage(initialName: initialName), Provider.value(
value: context.read<LabelRepository>(),
),
Provider.value(
value: context.read<ApiVersion>(),
)
],
child: AddDocumentTypePage(initialName: initialName),
),
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey,
options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateDocumentTypes,
), ),
addLabelText: S.of(context)!.addDocumentType, if (LocalUserAccount.current.paperlessUser.canViewTags)
labelText: S.of(context)!.documentType + " *", TagsFormField(
name: DocumentModel.documentTypeKey, name: DocumentModel.tagsKey,
options: state.documentTypes, allowCreation: true,
prefixIcon: const Icon(Icons.description_outlined), allowExclude: false,
allowSelectUnassigned: true, allowOnlySelection: true,
canCreateNewLabel: options: state.tags,
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
), ),
),
TagsFormField(
name: DocumentModel.tagsKey,
allowCreation: true,
allowExclude: false,
allowOnlySelection: true,
options: state.tags,
),
Text( Text(
"* " + S.of(context)!.uploadInferValuesHint, "* " + S.of(context)!.uploadInferValuesHint,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
@@ -301,14 +312,14 @@ class _DocumentUploadPreparationPageState
context, context,
DocumentUploadResult(true, taskId), DocumentUploadResult(true, taskId),
); );
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (errors) { } on PaperlessFormValidationException catch (exception) {
setState(() => _errors = errors); setState(() => _errors = exception.validationMessages);
} catch (unknownError, stackTrace) { } catch (unknownError, stackTrace) {
debugPrint(unknownError.toString()); debugPrint(unknownError.toString());
showErrorMessage( showErrorMessage(
context, const PaperlessServerException.unknown(), stackTrace); context, const PaperlessApiException.unknown(), stackTrace);
} finally { } finally {
setState(() { setState(() {
_isUploadLoading = false; _isUploadLoading = false;

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
@@ -53,14 +54,16 @@ class _DocumentsPageState extends State<DocumentsPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final showSavedViews =
LocalUserAccount.current.paperlessUser.canViewSavedViews;
_tabController = TabController( _tabController = TabController(
length: 2, length: showSavedViews ? 2 : 1,
vsync: this, vsync: this,
); );
Future.wait([ Future.wait([
context.read<DocumentsCubit>().reload(), context.read<DocumentsCubit>().reload(),
context.read<SavedViewCubit>().reload(), context.read<SavedViewCubit>().reload(),
]).onError<PaperlessServerException>( ]).onError<PaperlessApiException>(
(error, stackTrace) { (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
return []; return [];
@@ -105,7 +108,7 @@ class _DocumentsPageState extends State<DocumentsPage>
listener: (context, state) { listener: (context, state) {
try { try {
context.read<DocumentsCubit>().reload(); context.read<DocumentsCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
}, },
@@ -197,7 +200,10 @@ class _DocumentsPageState extends State<DocumentsPage>
sliver: BlocBuilder<DocumentsCubit, DocumentsState>( sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) { builder: (context, state) {
if (state.selection.isEmpty) { if (state.selection.isEmpty) {
return const SliverSearchBar(floating: true); return SliverSearchBar(
floating: true,
titleText: S.of(context)!.documents,
);
} else { } else {
return DocumentSelectionSliverAppBar( return DocumentSelectionSliverAppBar(
state: state, state: state,
@@ -226,7 +232,9 @@ class _DocumentsPageState extends State<DocumentsPage>
controller: _tabController, controller: _tabController,
tabs: [ tabs: [
Tab(text: S.of(context)!.documents), Tab(text: S.of(context)!.documents),
Tab(text: S.of(context)!.views), if (LocalUserAccount.current.paperlessUser
.canViewSavedViews)
Tab(text: S.of(context)!.views),
], ],
), ),
), ),
@@ -268,14 +276,16 @@ class _DocumentsPageState extends State<DocumentsPage>
); );
}, },
), ),
Builder( if (LocalUserAccount
builder: (context) { .current.paperlessUser.canViewSavedViews)
return _buildSavedViewsTab( Builder(
connectivityState, builder: (context) {
context, return _buildSavedViewsTab(
); connectivityState,
}, context,
), );
},
),
], ],
), ),
), ),
@@ -334,7 +344,7 @@ class _DocumentsPageState extends State<DocumentsPage>
context context
.read<DocumentsCubit>() .read<DocumentsCubit>()
.loadMore() .loadMore()
.onError<PaperlessServerException>( .onError<PaperlessApiException>(
(error, stackTrace) => showErrorMessage( (error, stackTrace) => showErrorMessage(
context, context,
error, error,
@@ -419,7 +429,7 @@ class _DocumentsPageState extends State<DocumentsPage>
if (newView != null) { if (newView != null) {
try { try {
await context.read<SavedViewCubit>().add(newView); await context.read<SavedViewCubit>().add(newView);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }
@@ -472,7 +482,7 @@ class _DocumentsPageState extends State<DocumentsPage>
.read<DocumentsCubit>() .read<DocumentsCubit>()
.updateFilter(filter: filterIntent.filter!); .updateFilter(filter: filterIntent.filter!);
} }
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, 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); 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); 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); 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); showErrorMessage(context, error, stackTrace);
} }
} }
@@ -626,7 +636,7 @@ class _DocumentsPageState extends State<DocumentsPage>
try { try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar. // We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<DocumentsCubit>().reload(); await context.read<DocumentsCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }
@@ -635,7 +645,7 @@ class _DocumentsPageState extends State<DocumentsPage>
try { try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar. // We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<SavedViewCubit>().reload(); await context.read<SavedViewCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }

View File

@@ -160,10 +160,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.documentType, initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: false, allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( canCreateNewLabel:
PermissionAction.add, LocalUserAccount.current.paperlessUser.canCreateDocumentTypes,
PermissionTarget.documentType,
),
); );
} }
@@ -175,10 +173,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.correspondent, initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: false, allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( canCreateNewLabel:
PermissionAction.add, LocalUserAccount.current.paperlessUser.canCreateCorrespondents,
PermissionTarget.correspondent,
),
); );
} }
@@ -190,10 +186,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.storagePath, initialValue: widget.initialFilter.storagePath,
prefixIcon: const Icon(Icons.folder_outlined), prefixIcon: const Icon(Icons.folder_outlined),
allowSelectUnassigned: false, allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission( canCreateNewLabel:
PermissionAction.add, LocalUserAccount.current.paperlessUser.canCreateStoragePaths,
PermissionTarget.storagePath,
),
); );
} }

View File

@@ -47,7 +47,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
S.of(context)!.documentsSuccessfullyDeleted, S.of(context)!.documentsSuccessfullyDeleted,
); );
context.read<DocumentsCubit>().resetSelection(); context.read<DocumentsCubit>().resetSelection();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }

View File

@@ -114,7 +114,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
if (shouldDelete) { if (shouldDelete) {
try { try {
onDelete(context, label); onDelete(context, label);
} on PaperlessServerException catch (error) { } on PaperlessApiException catch (error) {
showErrorMessage(context, error); showErrorMessage(context, error);
} catch (error, stackTrace) { } catch (error, stackTrace) {
log("An error occurred!", error: error, stackTrace: stackTrace); log("An error occurred!", error: error, stackTrace: stackTrace);

View File

@@ -24,10 +24,8 @@ class EditCorrespondentPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceCorrespondent(label), context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label), context.read<EditLabelCubit>().removeCorrespondent(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete:
PermissionAction.delete, LocalUserAccount.current.paperlessUser.canDeleteCorrespondents,
PermissionTarget.correspondent,
),
); );
}), }),
); );

View File

@@ -22,10 +22,8 @@ class EditDocumentTypePage extends StatelessWidget {
context.read<EditLabelCubit>().replaceDocumentType(label), context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label), context.read<EditLabelCubit>().removeDocumentType(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete:
PermissionAction.delete, LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes,
PermissionTarget.documentType,
),
), ),
); );
} }

View File

@@ -23,10 +23,7 @@ class EditStoragePathPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceStoragePath(label), context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label), context.read<EditLabelCubit>().removeStoragePath(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths,
PermissionAction.delete,
PermissionTarget.storagePath,
),
additionalFields: [ additionalFields: [
StoragePathAutofillFormBuilderField( StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey, name: StoragePath.pathKey,

View File

@@ -26,10 +26,7 @@ class EditTagPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceTag(label), context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label), context.read<EditLabelCubit>().removeTag(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission( canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags,
PermissionAction.delete,
PermissionTarget.tag,
),
additionalFields: [ additionalFields: [
FormBuilderColorPickerField( FormBuilderColorPickerField(
initialValue: tag.color, 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_api/paperless_api.dart';
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.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; late bool _enableMatchFormField;
PaperlessValidationErrors _errors = {}; Map<String, String> _errors = {};
@override @override
void initState() { void initState() {
@@ -69,7 +68,8 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<MatchingAlgorithm> selectableMatchingAlgorithmValues = List<MatchingAlgorithm> selectableMatchingAlgorithmValues =
getSelectableMatchingAlgorithmValues( getSelectableMatchingAlgorithmValues(
context.watch<ApiVersion>().hasMultiUserSupport); context.watch<ApiVersion>().hasMultiUserSupport,
);
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
@@ -168,10 +168,10 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
final parsed = widget.fromJsonT(mergedJson); final parsed = widget.fromJsonT(mergedJson);
final createdLabel = await widget.submitButtonConfig.onSubmit(parsed); final createdLabel = await widget.submitButtonConfig.onSubmit(parsed);
Navigator.pop(context, createdLabel); Navigator.pop(context, createdLabel);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (errors) { } on PaperlessFormValidationException catch (exception) {
setState(() => _errors = errors); setState(() => _errors = exception.validationMessages);
} }
} }
} }

View File

@@ -44,14 +44,14 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WidgetsBindingObserver { class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _currentIndex = 0; int _currentIndex = 0;
late Timer _inboxTimer; Timer? _inboxTimer;
late final StreamSubscription _shareMediaSubscription; late final StreamSubscription _shareMediaSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_listenToInboxChanges();
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings) final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()! .getValue()!
.currentLoggedInUser!; .currentLoggedInUser!;
@@ -73,13 +73,15 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} }
void _listenToInboxChanges() { void _listenToInboxChanges() {
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) { if (LocalUserAccount.current.paperlessUser.canViewTags) {
if (!mounted) { _inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
timer.cancel(); if (!mounted) {
} else { timer.cancel();
context.read<InboxCubit>().refreshItemsInInboxCount(); } else {
} context.read<InboxCubit>().refreshItemsInInboxCount();
}); }
});
}
} }
@override @override
@@ -89,7 +91,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
log('App is now in foreground'); log('App is now in foreground');
context.read<ConnectivityCubit>().reload(); context.read<ConnectivityCubit>().reload();
log("Reloaded device connectivity state"); log("Reloaded device connectivity state");
if (!_inboxTimer.isActive) { if (!(_inboxTimer?.isActive ?? true)) {
_listenToInboxChanges(); _listenToInboxChanges();
} }
break; break;
@@ -98,7 +100,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
case AppLifecycleState.detached: case AppLifecycleState.detached:
default: default:
log('App is now in background'); log('App is now in background');
_inboxTimer.cancel(); _inboxTimer?.cancel();
break; break;
} }
} }
@@ -106,7 +108,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_inboxTimer.cancel(); _inboxTimer?.cancel();
_shareMediaSubscription.cancel(); _shareMediaSubscription.cancel();
super.dispose(); super.dispose();
} }
@@ -158,8 +160,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
return; return;
} }
if (!LocalUserAccount.current.paperlessUser if (!LocalUserAccount.current.paperlessUser.canCreateDocuments) {
.hasPermission(PermissionAction.add, PermissionTarget.document)) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: "You do not have the permissions to upload documents.", 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, label: S.of(context)!.documents,
), ),
if (LocalUserAccount.current.paperlessUser if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
.hasPermission(PermissionAction.add, PermissionTarget.document))
RouteDescription( RouteDescription(
icon: const Icon(Icons.document_scanner_outlined), icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon( selectedIcon: Icon(
@@ -218,33 +218,31 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
), ),
label: S.of(context)!.labels, label: S.of(context)!.labels,
), ),
RouteDescription( if (LocalUserAccount.current.paperlessUser.canViewTags)
icon: const Icon(Icons.inbox_outlined), RouteDescription(
selectedIcon: Icon( icon: const Icon(Icons.inbox_outlined),
Icons.inbox, selectedIcon: Icon(
color: Theme.of(context).colorScheme.primary, Icons.inbox,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context)!.inbox,
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: icon,
);
},
),
), ),
label: S.of(context)!.inbox,
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: icon,
);
},
),
),
]; ];
final routes = <Widget>[ final routes = <Widget>[
const DocumentsPage(), const DocumentsPage(),
if (LocalUserAccount.current.paperlessUser.hasPermission( if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
PermissionAction.add,
PermissionTarget.document,
))
const ScannerPage(), const ScannerPage(),
const LabelsPage(), const LabelsPage(),
const InboxPage(), if (LocalUserAccount.current.paperlessUser.canViewTags) const InboxPage(),
]; ];
return MultiBlocListener( return MultiBlocListener(
listeners: [ listeners: [

View File

@@ -3,6 +3,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.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/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
@@ -43,7 +44,14 @@ class HomeRoute extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GlobalSettingsBuilder( return GlobalSettingsBuilder(
builder: (context, settings) { 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); final apiVersion = ApiVersion(paperlessApiVersion);
return MultiProvider( return MultiProvider(
providers: [ providers: [
@@ -104,12 +112,31 @@ class HomeRoute extends StatelessWidget {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ProxyProvider<PaperlessLabelsApi, LabelRepository>( ProxyProvider<PaperlessLabelsApi, LabelRepository>(
update: (context, value, previous) => update: (context, value, previous) {
LabelRepository(value)..initialize(), 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>( ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
update: (context, value, previous) => update: (context, value, previous) {
SavedViewRepository(value)..initialize(), final repo = SavedViewRepository(value);
if (currentUser.paperlessUser.canViewSavedViews) {
repo.initialize();
}
return repo;
},
), ),
], ],
builder: (context, child) { builder: (context, child) {

View File

@@ -38,8 +38,8 @@ class _InboxPageState extends State<InboxPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final canEditDocument = LocalUserAccount.current.paperlessUser final canEditDocument =
.hasPermission(PermissionAction.change, PermissionTarget.document); LocalUserAccount.current.paperlessUser.canEditDocuments;
return Scaffold( return Scaffold(
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>( floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
@@ -65,7 +65,9 @@ class _InboxPageState extends State<InboxPage>
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: searchBarHandle, handle: searchBarHandle,
sliver: const SliverSearchBar(), sliver: SliverSearchBar(
titleText: S.of(context)!.inbox,
),
) )
], ],
body: BlocBuilder<InboxCubit, InboxState>( body: BlocBuilder<InboxCubit, InboxState>(
@@ -222,14 +224,14 @@ class _InboxPageState extends State<InboxPage>
), ),
); );
return true; return true;
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} on ServerMessageException catch (error) { } on ServerMessageException catch (error) {
showGenericError(context, error.message); showGenericError(context, error.message);
} catch (error) { } catch (error) {
showErrorMessage( showErrorMessage(
context, context,
const PaperlessServerException.unknown(), const PaperlessApiException.unknown(),
); );
} }
return false; return false;
@@ -243,7 +245,7 @@ class _InboxPageState extends State<InboxPage>
await context await context
.read<InboxCubit>() .read<InboxCubit>()
.undoRemoveFromInbox(document, removedTags); .undoRemoveFromInbox(document, removedTags);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }

View File

@@ -238,10 +238,8 @@ class _InboxItemState extends State<InboxItem> {
} }
Widget _buildActions(BuildContext context) { Widget _buildActions(BuildContext context) {
final canEdit = LocalUserAccount.current.paperlessUser final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments;
.hasPermission(PermissionAction.change, PermissionTarget.document); final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments;
final canDelete = LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.delete, PermissionTarget.document);
final chipShape = RoundedRectangleBorder( final chipShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
); );

View File

@@ -73,10 +73,7 @@ class TagsFormField extends StatelessWidget {
initialValue: field.value, initialValue: field.value,
allowOnlySelection: allowOnlySelection, allowOnlySelection: allowOnlySelection,
allowCreation: allowCreation && allowCreation: allowCreation &&
LocalUserAccount.current.paperlessUser.hasPermission( LocalUserAccount.current.paperlessUser.canCreateTags,
PermissionAction.add,
PermissionTarget.tag,
),
allowExclude: allowExclude, allowExclude: allowExclude,
), ),
onClosed: (data) { onClosed: (data) {

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/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/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
@@ -39,291 +42,327 @@ class _LabelsPageState extends State<LabelsPage>
late final TabController _tabController; late final TabController _tabController;
int _currentIndex = 0; int _currentIndex = 0;
int _calculateTabCount(UserModel user) => [
user.canViewCorrespondents,
user.canViewDocumentTypes,
user.canViewTags,
user.canViewStoragePaths,
].fold(0, (value, element) => value + (element ? 1 : 0));
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final user = LocalUserAccount.current.paperlessUser;
_tabController = TabController(length: 4, vsync: this) _tabController = TabController(
length: _calculateTabCount(user), vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index)); ..addListener(() => setState(() => _currentIndex = _tabController.index));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController( return ValueListenableBuilder(
length: 3, valueListenable:
child: BlocBuilder<ConnectivityCubit, ConnectivityState>( Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
builder: (context, connectedState) { builder: (context, box, child) {
return SafeArea( final currentUserId =
child: Scaffold( Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
drawer: const AppDrawer(), .getValue()!
floatingActionButton: FloatingActionButton( .currentLoggedInUser;
onPressed: [ final user = box.get(currentUserId)!.paperlessUser;
_openAddCorrespondentPage,
_openAddDocumentTypePage, return BlocBuilder<ConnectivityCubit, ConnectivityState>(
_openAddTagPage, builder: (context, connectedState) {
_openAddStoragePathPage, return SafeArea(
][_currentIndex], child: Scaffold(
child: const Icon(Icons.add), drawer: const AppDrawer(),
), floatingActionButton: FloatingActionButton(
body: NestedScrollView( onPressed: [
floatHeaderSlivers: true, if (user.canViewCorrespondents) _openAddCorrespondentPage,
headerSliverBuilder: (context, innerBoxIsScrolled) => [ if (user.canViewDocumentTypes) _openAddDocumentTypePage,
SliverOverlapAbsorber( if (user.canViewTags) _openAddTagPage,
handle: searchBarHandle, if (user.canViewStoragePaths) _openAddStoragePathPage,
sliver: const SliverSearchBar(), ][_currentIndex],
child: const Icon(Icons.add),
), ),
SliverOverlapAbsorber( body: NestedScrollView(
handle: tabBarHandle, floatHeaderSlivers: true,
sliver: SliverPersistentHeader( headerSliverBuilder: (context, innerBoxIsScrolled) => [
pinned: true, SliverOverlapAbsorber(
delegate: CustomizableSliverPersistentHeaderDelegate( handle: searchBarHandle,
child: ColoredTabBar( sliver: SliverSearchBar(
tabBar: TabBar( titleText: S.of(context)!.labels,
),
),
SliverOverlapAbsorber(
handle: tabBarHandle,
sliver: SliverPersistentHeader(
pinned: true,
delegate: CustomizableSliverPersistentHeaderDelegate(
child: ColoredTabBar(
tabBar: TabBar(
controller: _tabController,
tabs: [
if (user.canViewCorrespondents)
Tab(
icon: Tooltip(
message: S.of(context)!.correspondents,
child: Icon(
Icons.person_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewDocumentTypes)
Tab(
icon: Tooltip(
message: S.of(context)!.documentTypes,
child: Icon(
Icons.description_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewTags)
Tab(
icon: Tooltip(
message: S.of(context)!.tags,
child: Icon(
Icons.label_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewStoragePaths)
Tab(
icon: Tooltip(
message: S.of(context)!.storagePaths,
child: Icon(
Icons.folder_open,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
],
),
),
minExtent: kTextTabBarHeight,
maxExtent: kTextTabBarHeight,
),
),
),
],
body: BlocBuilder<LabelCubit, LabelState>(
builder: (context, state) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
}
final desiredTab =
((metrics.pixels / metrics.maxScrollExtent) *
(_tabController.length - 1))
.round();
if (metrics.axis == Axis.horizontal &&
_currentIndex != desiredTab) {
setState(() => _currentIndex = desiredTab);
}
return true;
},
child: RefreshIndicator(
edgeOffset: kTextTabBarHeight,
notificationPredicate: (notification) =>
connectedState.isConnected,
onRefresh: () async {
try {
await [
context
.read<LabelCubit>()
.reloadCorrespondents,
context
.read<LabelCubit>()
.reloadDocumentTypes,
context.read<LabelCubit>().reloadTags,
context.read<LabelCubit>().reloadStoragePaths,
][_currentIndex]
.call();
} catch (error, stackTrace) {
debugPrint(
"[LabelsPage] RefreshIndicator.onRefresh "
"${[
"correspondents",
"document types",
"tags",
"storage paths"
][_currentIndex]}: "
"An error occurred (${error.toString()})",
);
debugPrintStack(stackTrace: stackTrace);
}
},
child: TabBarView(
controller: _tabController, controller: _tabController,
tabs: [ children: [
Tab( if (user.canViewCorrespondents)
icon: Icon( Builder(
Icons.person_outline, builder: (context) {
color: Theme.of(context) return CustomScrollView(
.colorScheme slivers: [
.onPrimaryContainer, SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: state.correspondents,
filterBuilder: (label) =>
DocumentFilter(
correspondent:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditCorrespondents,
canAddNew:
user.canCreateCorrespondents,
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel: S
.of(context)!
.addNewCorrespondent,
emptyStateDescription: S
.of(context)!
.noCorrespondentsSetUp,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
), ),
), if (user.canViewDocumentTypes)
Tab( Builder(
icon: Icon( builder: (context) {
Icons.description_outlined, return CustomScrollView(
color: Theme.of(context) slivers: [
.colorScheme SliverOverlapInjector(
.onPrimaryContainer, handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: state.documentTypes,
filterBuilder: (label) =>
DocumentFilter(
documentType:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditDocumentTypes,
canAddNew:
user.canCreateDocumentTypes,
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel: S
.of(context)!
.addNewDocumentType,
emptyStateDescription: S
.of(context)!
.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
), ),
), if (user.canViewTags)
Tab( Builder(
icon: Icon( builder: (context) {
Icons.label_outline, return CustomScrollView(
color: Theme.of(context) slivers: [
.colorScheme SliverOverlapInjector(
.onPrimaryContainer, handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Tag>(
labels: state.tags,
filterBuilder: (label) =>
DocumentFilter(
tags: TagsQuery.ids(
include: [label.id!]),
),
canEdit: user.canEditTags,
canAddNew: user.canCreateTags,
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel:
S.of(context)!.addNewTag,
emptyStateDescription:
S.of(context)!.noTagsSetUp,
onAddNew: _openAddTagPage,
),
],
);
},
), ),
), if (user.canViewStoragePaths)
Tab( Builder(
icon: Icon( builder: (context) {
Icons.folder_open, return CustomScrollView(
color: Theme.of(context) slivers: [
.colorScheme SliverOverlapInjector(
.onPrimaryContainer, handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: state.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) =>
DocumentFilter(
storagePath:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditStoragePaths,
canAddNew:
user.canCreateStoragePaths,
contentBuilder: (path) =>
Text(path.path),
emptyStateActionButtonLabel: S
.of(context)!
.addNewStoragePath,
emptyStateDescription: S
.of(context)!
.noStoragePathsSetUp,
onAddNew: _openAddStoragePathPage,
),
],
);
},
), ),
),
], ],
), ),
), ),
minExtent: kTextTabBarHeight, );
maxExtent: kTextTabBarHeight), },
), ),
), ),
],
body: BlocBuilder<LabelCubit, LabelState>(
builder: (context, state) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
}
final desiredTab =
((metrics.pixels / metrics.maxScrollExtent) *
(_tabController.length - 1))
.round();
if (metrics.axis == Axis.horizontal &&
_currentIndex != desiredTab) {
setState(() => _currentIndex = desiredTab);
}
return true;
},
child: RefreshIndicator(
edgeOffset: kTextTabBarHeight,
notificationPredicate: (notification) =>
connectedState.isConnected,
onRefresh: () async {
try {
await [
context.read<LabelCubit>().reloadCorrespondents,
context.read<LabelCubit>().reloadDocumentTypes,
context.read<LabelCubit>().reloadTags,
context.read<LabelCubit>().reloadStoragePaths,
][_currentIndex]
.call();
} catch (error, stackTrace) {
debugPrint(
"[LabelsPage] RefreshIndicator.onRefresh "
"${[
"correspondents",
"document types",
"tags",
"storage paths"
][_currentIndex]}: "
"An error occurred (${error.toString()})",
);
debugPrintStack(stackTrace: stackTrace);
}
},
child: TabBarView(
controller: _tabController,
children: [
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: state.correspondents,
filterBuilder: (label) => DocumentFilter(
correspondent:
IdQueryParameter.fromId(label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.correspondent),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.correspondent),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel:
S.of(context)!.addNewCorrespondent,
emptyStateDescription:
S.of(context)!.noCorrespondentsSetUp,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: state.documentTypes,
filterBuilder: (label) => DocumentFilter(
documentType:
IdQueryParameter.fromId(label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.documentType),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.documentType),
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel:
S.of(context)!.addNewDocumentType,
emptyStateDescription:
S.of(context)!.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Tag>(
labels: state.tags,
filterBuilder: (label) => DocumentFilter(
tags:
TagsQuery.ids(include: [label.id!]),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.tag),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.tag),
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel:
S.of(context)!.addNewTag,
emptyStateDescription:
S.of(context)!.noTagsSetUp,
onAddNew: _openAddTagPage,
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: state.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath:
IdQueryParameter.fromId(label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.storagePath),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.storagePath),
contentBuilder: (path) => Text(path.path),
emptyStateActionButtonLabel:
S.of(context)!.addNewStoragePath,
emptyStateDescription:
S.of(context)!.noStoragePathsSetUp,
onAddNew: _openAddStoragePathPage,
),
],
);
},
),
],
),
),
);
},
), ),
), );
), },
); );
}, });
),
);
} }
void _openEditCorrespondentPage(Correspondent correspondent) { void _openEditCorrespondentPage(Correspondent correspondent) {

View File

@@ -36,8 +36,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
Widget _buildReferencedDocumentsWidget(BuildContext context) { Widget _buildReferencedDocumentsWidget(BuildContext context) {
final canOpen = (label.documentCount ?? 0) > 0 && final canOpen = (label.documentCount ?? 0) > 0 &&
LocalUserAccount.current.paperlessUser LocalUserAccount.current.paperlessUser.canViewDocuments;
.hasPermission(PermissionAction.view, PermissionTarget.document);
return TextButton.icon( return TextButton.icon(
label: const Icon(Icons.link), label: const Icon(Icons.link),
icon: Text(formatMaxCount(label.documentCount)), icon: Text(formatMaxCount(label.documentCount)),

View File

@@ -365,7 +365,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
apiVersion: apiVersion, apiVersion: apiVersion,
) )
.findCurrentUser(); .findCurrentUser();
} on DioError catch (error, stackTrace) { } on DioException catch (error, stackTrace) {
_debugPrintMessage( _debugPrintMessage(
"_addUser", "_addUser",
"An error occurred: ${error.message}", "An error occurred: ${error.message}",

View File

@@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
ServerConnectionPage( ServerConnectionPage(
titleString: widget.titleString, titleText: widget.titleString,
formBuilderKey: _formKey, formBuilderKey: _formKey,
onContinue: () { onContinue: () {
_pageController.nextPage( _pageController.nextPage(
@@ -126,7 +126,6 @@ class _LoginPageState extends State<LoginPage> {
} }
Future<void> _login() async { Future<void> _login() async {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final form = _formKey.currentState!.value; final form = _formKey.currentState!.value;
@@ -150,7 +149,7 @@ class _LoginPageState extends State<LoginPage> {
form[ServerAddressFormField.fkServerAddress], form[ServerAddressFormField.fkServerAddress],
clientCert, clientCert,
); );
} on PaperlessServerException catch (error) { } on PaperlessApiException catch (error) {
showErrorMessage(context, error); showErrorMessage(context, error);
} on ServerMessageException catch (error) { } on ServerMessageException catch (error) {
showLocalizedError(context, error.message); showLocalizedError(context, error.message);

View File

@@ -66,7 +66,10 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
.values .values
.where((element) => element.contains(textEditingValue.text)); .where((element) => element.contains(textEditingValue.text));
}, },
onSelected: (option) => _formatInput(), onSelected: (option) {
_formatInput();
field.didChange(_textEditingController.text);
},
fieldViewBuilder: fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) { (context, textEditingController, focusNode, onFieldSubmitted) {
return TextField( return TextField(
@@ -111,6 +114,10 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
String address = _textEditingController.text.trim(); String address = _textEditingController.text.trim();
address = address.replaceAll(RegExp(r'^\/+|\/+$'), ''); address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
_textEditingController.text = address; _textEditingController.text = address;
_textEditingController.selection = TextSelection(
baseOffset: address.length,
extentOffset: address.length,
);
widget.onSubmit(address); widget.onSubmit(address);
} }
} }

View File

@@ -13,14 +13,14 @@ import 'package:provider/provider.dart';
class ServerConnectionPage extends StatefulWidget { class ServerConnectionPage extends StatefulWidget {
final GlobalKey<FormBuilderState> formBuilderKey; final GlobalKey<FormBuilderState> formBuilderKey;
final void Function() onContinue; final VoidCallback onContinue;
final String titleString; final String titleText;
const ServerConnectionPage({ const ServerConnectionPage({
super.key, super.key,
required this.formBuilderKey, required this.formBuilderKey,
required this.onContinue, required this.onContinue,
required this.titleString, required this.titleText,
}); });
@override @override
@@ -36,7 +36,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
toolbarHeight: kToolbarHeight - 4, toolbarHeight: kToolbarHeight - 4,
title: Text(widget.titleString), title: Text(widget.titleText),
bottom: PreferredSize( bottom: PreferredSize(
child: _isCheckingConnection child: _isCheckingConnection
? const LinearProgressIndicator() ? const LinearProgressIndicator()

View File

@@ -26,7 +26,7 @@ mixin DocumentPagingViewMixin<T extends StatefulWidget,
if (shouldLoadMoreDocuments) { if (shouldLoadMoreDocuments) {
try { try {
await _bloc.loadMore(); await _bloc.loadMore();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }

View File

@@ -27,7 +27,7 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
super.initState(); super.initState();
try { try {
context.read<SimilarDocumentsCubit>().initialize(); context.read<SimilarDocumentsCubit>().initialize();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }

View File

@@ -92,7 +92,7 @@ void showLocalizedError(
void showErrorMessage( void showErrorMessage(
BuildContext context, BuildContext context,
PaperlessServerException error, [ PaperlessApiException error, [
StackTrace? stackTrace, StackTrace? stackTrace,
]) { ]) {
showSnackBar( 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/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/security/session_manager.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/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/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/home/view/home_route.dart'; import 'package:paperless_mobile/features/home/view/home_route.dart';
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
@@ -168,7 +167,7 @@ void main() async {
); );
}, (error, stack) { }, (error, stack) {
String message = switch (error) { String message = switch (error) {
PaperlessServerException e => e.details ?? error.toString(), PaperlessApiException e => e.details ?? error.toString(),
ServerMessageException e => e.message, ServerMessageException e => e.message,
_ => error.toString() _ => error.toString()
}; };
@@ -315,8 +314,10 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
) async { ) async {
try { try {
await context.read<AuthenticationCubit>().login( await context.read<AuthenticationCubit>().login(
credentials: credentials: LoginFormCredentials(
LoginFormCredentials(username: username, password: password), username: username,
password: password,
),
serverUrl: serverUrl, serverUrl: serverUrl,
clientCertificate: clientCertificate, clientCertificate: clientCertificate,
); );
@@ -335,13 +336,17 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
globalSettings.save(); globalSettings.save();
}); });
} }
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (error, stackTrace) { } on PaperlessFormValidationException catch (exception, stackTrace) {
if (error.hasFieldUnspecificError) { if (exception.hasUnspecificErrorMessage()) {
showLocalizedError(context, error.fieldUnspecificError!); showLocalizedError(context, exception.unspecificErrorMessage()!);
} else { } 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) { } catch (unknownError, stackTrace) {
showGenericError(context, unknownError.toString(), 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/storage_path_model.dart';
export 'labels/tag_model.dart'; export 'labels/tag_model.dart';
export 'paged_search_result.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_information_model.dart';
export 'paperless_server_statistics_model.dart'; export 'paperless_server_statistics_model.dart';
export 'permissions/inherited_permissions.dart'; export 'permissions/inherited_permissions.dart';
@@ -31,3 +31,4 @@ export 'saved_view_model.dart';
export 'task/task.dart'; export 'task/task.dart';
export 'task/task_status.dart'; export 'task/task_status.dart';
export 'user_model.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 ErrorCode code;
final String? details; final String? details;
final StackTrace? stackTrace; final StackTrace? stackTrace;
final int? httpStatusCode; final int? httpStatusCode;
const PaperlessServerException( const PaperlessApiException(
this.code, { this.code, {
this.details, this.details,
this.stackTrace, this.stackTrace,
this.httpStatusCode, this.httpStatusCode,
}); });
const PaperlessServerException.unknown() : this(ErrorCode.unknown); const PaperlessApiException.unknown() : this(ErrorCode.unknown);
@override @override
String toString() { String toString() {
@@ -53,5 +53,6 @@ enum ErrorCode {
requestTimedOut, requestTimedOut,
unsupportedFileFormat, unsupportedFileFormat,
missingClientCertificate, 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) { v3: (user) {
final permission = [action.value, target.value].join("_"); final permission = [action.value, target.value].join("_");
return user.userPermissions.any((element) => element == permission) || 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, v2: (_) => true,
); );
} }
bool hasPermissions(List<PermissionAction> actions, List<PermissionTarget> targets) { bool hasPermissions(
List<PermissionAction> actions, List<PermissionTarget> targets) {
return map( return map(
v3: (user) { v3: (user) {
final permissions = [ final permissions = [
@@ -21,10 +23,62 @@ extension UserPermissionExtension on UserModel {
]; ];
return permissions.every((requestedPermission) => return permissions.every((requestedPermission) =>
user.userPermissions.contains(requestedPermission) || user.userPermissions.contains(requestedPermission) ||
user.inheritedPermissions user.inheritedPermissions.any(
.any((element) => element.split(".").last == requestedPermission)); (element) => element.split(".").last == requestedPermission));
}, },
v2: (_) => true, 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 { abstract class PaperlessAuthenticationApi {
///
/// @throws [PaperlessUnauthorizedException]
///
Future<String> login({ Future<String> login({
required String username, required String username,
required String password, required String password,

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,12 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; 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/correspondent_model.dart';
import 'package:paperless_api/src/models/labels/document_type_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/storage_path_model.dart';
import 'package:paperless_api/src/models/labels/tag_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/modules/labels_api/paperless_labels_api.dart';
import 'package:paperless_api/src/request_utils.dart'; import 'package:paperless_api/src/request_utils.dart';
@@ -94,16 +95,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post( final response = await _client.post(
'/api/correspondents/', '/api/correspondents/',
data: correspondent.toJson(), data: correspondent.toJson(),
options: Options(validateStatus: (status) => status == 201),
); );
if (response.statusCode == HttpStatus.created) { return Correspondent.fromJson(response.data);
return Correspondent.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(
ErrorCode.correspondentCreateFailed, 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( final response = await _client.post(
'/api/document_types/', '/api/document_types/',
data: type.toJson(), data: type.toJson(),
options: Options(
validateStatus: (status) => status == 201,
),
); );
if (response.statusCode == HttpStatus.created) { return DocumentType.fromJson(response.data);
return DocumentType.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(
ErrorCode.documentTypeCreateFailed, 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( final response = await _client.post(
'/api/tags/', '/api/tags/',
data: tag.toJson(), 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);
return Tag.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(
ErrorCode.tagCreateFailed, 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 { Future<int> deleteCorrespondent(Correspondent correspondent) async {
assert(correspondent.id != null); assert(correspondent.id != null);
try { try {
final response = await _client.delete(
await _client.delete('/api/correspondents/${correspondent.id}/'); '/api/correspondents/${correspondent.id}/',
if (response.statusCode == HttpStatus.noContent) { options: Options(validateStatus: (status) => status == 204),
return correspondent.id!; );
} return correspondent.id!;
throw PaperlessServerException( } on DioException catch (exception) {
ErrorCode.unknown, throw exception.unravel(
httpStatusCode: response.statusCode, 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 { Future<int> deleteDocumentType(DocumentType documentType) async {
assert(documentType.id != null); assert(documentType.id != null);
try { try {
final response = final response = await _client.delete(
await _client.delete('/api/document_types/${documentType.id}/'); '/api/document_types/${documentType.id}/',
if (response.statusCode == HttpStatus.noContent) { options: Options(validateStatus: (status) => status == 204),
return documentType.id!; );
} return documentType.id!;
throw PaperlessServerException( } on DioException catch (exception) {
ErrorCode.unknown, throw exception.unravel(
httpStatusCode: response.statusCode, 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 { Future<int> deleteTag(Tag tag) async {
assert(tag.id != null); assert(tag.id != null);
try { try {
final response = await _client.delete('/api/tags/${tag.id}/'); await _client.delete(
if (response.statusCode == HttpStatus.noContent) { '/api/tags/${tag.id}/',
return tag.id!; options: Options(validateStatus: (status) => status == 204),
} );
throw PaperlessServerException( return tag.id!;
ErrorCode.unknown, } on DioException catch (exception) {
httpStatusCode: response.statusCode, 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( final response = await _client.put(
'/api/correspondents/${correspondent.id}/', '/api/correspondents/${correspondent.id}/',
data: json.encode(correspondent.toJson()), data: json.encode(correspondent.toJson()),
options: Options(validateStatus: (status) => status == 200),
); );
if (response.statusCode == HttpStatus.ok) { return Correspondent.fromJson(response.data);
return Correspondent.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(
ErrorCode.unknown, //TODO: Add correct error code mapping. ErrorCode.correspondentUpdateFailed,
httpStatusCode: response.statusCode, ),
); );
} on DioError catch (err) {
throw err.error!;
} }
} }
@@ -226,16 +228,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.put( final response = await _client.put(
'/api/document_types/${documentType.id}/', '/api/document_types/${documentType.id}/',
data: documentType.toJson(), data: documentType.toJson(),
options: Options(validateStatus: (status) => status == 200),
); );
if (response.statusCode == HttpStatus.ok) { return DocumentType.fromJson(response.data);
return DocumentType.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(
ErrorCode.unknown, ErrorCode.documentTypeUpdateFailed,
httpStatusCode: response.statusCode, ),
); );
} on DioError catch (err) {
throw err.error!;
} }
} }
@@ -245,18 +246,19 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
try { try {
final response = await _client.put( final response = await _client.put(
'/api/tags/${tag.id}/', '/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(), data: tag.toJson(),
); );
if (response.statusCode == HttpStatus.ok) { return Tag.fromJson(response.data);
return Tag.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(
ErrorCode.unknown, ErrorCode.tagUpdateFailed,
httpStatusCode: response.statusCode, ),
); );
} on DioError catch (err) {
throw err.error!;
} }
} }
@@ -264,16 +266,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteStoragePath(StoragePath path) async { Future<int> deleteStoragePath(StoragePath path) async {
assert(path.id != null); assert(path.id != null);
try { try {
final response = await _client.delete('/api/storage_paths/${path.id}/'); final response = await _client.delete(
if (response.statusCode == HttpStatus.noContent) { '/api/storage_paths/${path.id}/',
return path.id!; options: Options(validateStatus: (status) => status == 204),
} );
throw PaperlessServerException( return path.id!;
ErrorCode.unknown, } on DioException catch (exception) {
httpStatusCode: response.statusCode, 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( final response = await _client.post(
'/api/storage_paths/', '/api/storage_paths/',
data: path.toJson(), data: path.toJson(),
options: Options(validateStatus: (status) => status == 201),
); );
if (response.statusCode == HttpStatus.created) { return StoragePath.fromJson(response.data);
return StoragePath.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(
ErrorCode.storagePathCreateFailed, 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( final response = await _client.put(
'/api/storage_paths/${path.id}/', '/api/storage_paths/${path.id}/',
data: path.toJson(), data: path.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
return StoragePath.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.storagePathUpdateFailed,
),
); );
if (response.statusCode == HttpStatus.ok) {
return StoragePath.fromJson(response.data);
}
throw const PaperlessServerException(ErrorCode.unknown);
} on DioError catch (err) {
throw err.error!;
} }
} }
} }

View File

@@ -1,7 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; 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/models/saved_view_model.dart';
import 'package:paperless_api/src/request_utils.dart'; import 'package:paperless_api/src/request_utils.dart';
@@ -30,32 +31,28 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
final response = await _client.post( final response = await _client.post(
"/api/saved_views/", "/api/saved_views/",
data: view.toJson(), data: view.toJson(),
options: Options(validateStatus: (status) => status == 201),
); );
if (response.statusCode == HttpStatus.created) { return SavedView.fromJson(response.data);
return SavedView.fromJson(response.data); } on DioException catch (exception) {
} throw exception.unravel(
throw PaperlessServerException( orElse: const PaperlessApiException(ErrorCode.createSavedViewError),
ErrorCode.createSavedViewError,
httpStatusCode: response.statusCode,
); );
} on DioError catch (err) {
throw err.error!;
} }
} }
@override @override
Future<int> delete(SavedView view) async { Future<int> delete(SavedView view) async {
try { try {
final response = await _client.delete("/api/saved_views/${view.id}/"); await _client.delete(
if (response.statusCode == HttpStatus.noContent) { "/api/saved_views/${view.id}/",
return view.id!; options: Options(validateStatus: (status) => status == 204),
} );
throw PaperlessServerException( return view.id!;
ErrorCode.deleteSavedViewError, } on DioException catch (exception) {
httpStatusCode: response.statusCode, 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: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_information_model.dart';
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart'; import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart'; import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
@@ -18,8 +19,11 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
@override @override
Future<PaperlessServerInformationModel> getServerInformation() async { Future<PaperlessServerInformationModel> getServerInformation() async {
final response = await client.get("/api/remote_version/"); try {
if (response.statusCode == 200) { final response = await client.get(
"/api/remote_version/",
options: Options(validateStatus: (status) => status == 200),
);
final version = response.data["version"] as String; final version = response.data["version"] as String;
final updateAvailable = response.data["update_available"] as bool; final updateAvailable = response.data["update_available"] as bool;
return PaperlessServerInformationModel( return PaperlessServerInformationModel(
@@ -27,25 +31,44 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
version: version, version: version,
isUpdateAvailable: updateAvailable, isUpdateAvailable: updateAvailable,
); );
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.serverInformationLoadFailed,
),
);
} }
throw const PaperlessServerException.unknown();
} }
@override @override
Future<PaperlessServerStatisticsModel> getServerStatistics() async { Future<PaperlessServerStatisticsModel> getServerStatistics() async {
final response = await client.get('/api/statistics/'); try {
if (response.statusCode == 200) { final response = await client.get(
'/api/statistics/',
options: Options(validateStatus: (status) => status == 200),
);
return PaperlessServerStatisticsModel.fromJson(response.data); return PaperlessServerStatisticsModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.serverStatisticsLoadFailed,
),
);
} }
throw const PaperlessServerException.unknown();
} }
@override @override
Future<PaperlessUiSettingsModel> getUiSettings() async { Future<PaperlessUiSettingsModel> getUiSettings() async {
final response = await client.get("/api/ui_settings/"); try {
if (response.statusCode == 200) { final response = await client.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return PaperlessUiSettingsModel.fromJson(response.data); 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:dio/dio.dart';
import 'package:paperless_api/paperless_api.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 { class PaperlessTasksApiImpl implements PaperlessTasksApi {
final Dio _client; final Dio _client;
@@ -41,11 +43,17 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
@override @override
Future<Iterable<Task>> findAll([Iterable<int>? ids]) async { Future<Iterable<Task>> findAll([Iterable<int>? ids]) async {
final response = await _client.get("/api/tasks/"); try {
if (response.statusCode == 200) { final response = await _client.get(
"/api/tasks/",
options: Options(validateStatus: (status) => status == 200),
);
return (response.data as List).map((e) => Task.fromJson(e)); return (response.data as List).map((e) => Task.fromJson(e));
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.loadTasksError),
);
} }
return [];
} }
@override @override
@@ -74,15 +82,22 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
@override @override
Future<Iterable<Task>> acknowledgeTasks(Iterable<Task> tasks) async { Future<Iterable<Task>> acknowledgeTasks(Iterable<Task> tasks) async {
final response = await _client.post("/api/acknowledge_tasks/", data: { try {
'tasks': tasks.map((e) => e.id).toList(), final response = await _client.post(
}); "/api/acknowledge_tasks/",
if (response.statusCode == 200) { data: {
'tasks': tasks.map((e) => e.id).toList(),
},
options: Options(validateStatus: (status) => status == 200),
);
if (response.data['result'] != tasks.length) { 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(); 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:dio/dio.dart';
import 'package:paperless_api/paperless_api.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 { class PaperlessUserApiV2Impl implements PaperlessUserApi {
final Dio client; final Dio client;
@@ -8,19 +10,33 @@ class PaperlessUserApiV2Impl implements PaperlessUserApi {
@override @override
Future<int> findCurrentUserId() async { Future<int> findCurrentUserId() async {
final response = await client.get("/api/ui_settings/"); try {
if (response.statusCode == 200) { final response = await client.get(
"/api/ui_settings/",
options: Options(
validateStatus: (status) => status == 200,
),
);
return response.data['user_id']; return response.data['user_id'];
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
} }
throw const PaperlessServerException.unknown();
} }
@override @override
Future<UserModel> findCurrentUser() async { Future<UserModel> findCurrentUser() async {
final response = await client.get("/api/ui_settings/"); try {
if (response.statusCode == 200) { final response = await client.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return UserModelV2.fromJson(response.data); 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:dio/dio.dart';
import 'package:paperless_api/paperless_api.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 { class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
final Dio dio; final Dio dio;
@@ -8,11 +10,17 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
@override @override
Future<UserModelV3> find(int id) async { Future<UserModelV3> find(int id) async {
final response = await dio.get("/api/users/$id/"); try {
if (response.statusCode == 200) { final response = await dio.get(
"/api/users/$id/",
options: Options(validateStatus: (status) => status == 200),
);
return UserModelV3.fromJson(response.data); return UserModelV3.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
} }
throw const PaperlessServerException.unknown();
} }
@override @override
@@ -22,40 +30,59 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
String contains = '', String contains = '',
String username = '', String username = '',
}) async { }) async {
final response = await dio.get("/api/users/", queryParameters: { try {
"username__istartswith": startsWith, final response = await dio.get(
"username__iendswith": endsWith, "/api/users/",
"username__icontains": contains, queryParameters: {
"username__iexact": username, "username__istartswith": startsWith,
}); "username__iendswith": endsWith,
if (response.statusCode == 200) { "username__icontains": contains,
"username__iexact": username,
},
options: Options(validateStatus: (status) => status == 200),
);
return PagedSearchResult<UserModelV3>.fromJson( return PagedSearchResult<UserModelV3>.fromJson(
response.data, response.data,
UserModelV3.fromJson as UserModelV3 Function(Object?), UserModelV3.fromJson as UserModelV3 Function(Object?),
).results; ).results;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
} }
throw const PaperlessServerException.unknown();
} }
@override @override
Future<int> findCurrentUserId() async { Future<int> findCurrentUserId() async {
final response = await dio.get("/api/ui_settings/"); try {
if (response.statusCode == 200) { final response = await dio.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return response.data['user']['id']; return response.data['user']['id'];
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
} }
throw const PaperlessServerException.unknown();
} }
@override @override
Future<Iterable<UserModelV3>> findAll() async { Future<Iterable<UserModelV3>> findAll() async {
final response = await dio.get("/api/users/"); try {
if (response.statusCode == 200) { final response = await dio.get(
"/api/users/",
options: Options(validateStatus: (status) => status == 200),
);
return PagedSearchResult<UserModelV3>.fromJson( return PagedSearchResult<UserModelV3>.fromJson(
response.data, response.data,
(json) => UserModelV3.fromJson(json as dynamic), (json) => UserModelV3.fromJson(json as dynamic),
).results; ).results;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
} }
throw const PaperlessServerException.unknown();
} }
@override @override

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,9 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/services.dart'; 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) { if (id != null) {
final coll = await loadCollection(filePath, transformFn); final coll = await loadCollection(filePath, transformFn);
return coll.firstWhere((dynamic element) => element.id == id); 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)); 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 { {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 String response = await rootBundle.loadString(filePath);
final lst = (jsonDecode(response) as List<dynamic>); 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) { if (ids != null) {
return res.where((dynamic element) => ids.contains(element.id)).toList(); return res.where((dynamic element) => ids.contains(element.id)).toList();
} }
if (numItems != null && lst.length < numItems) { 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 { } else {
return res.sublist(0, numItems); return res.sublist(0, numItems);
} }
} }
const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
String getRandomString(int length) => String.fromCharCodes( String getRandomString(int length) => String.fromCharCodes(Iterable.generate(
Iterable.generate(length, (_) => _chars.codeUnitAt(Random().nextInt(_chars.length)))); length, (_) => _chars.codeUnitAt(Random().nextInt(_chars.length))));