diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 308ee41..3c22aad 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,8 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0c93ae0..fdd3b40 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ - + + + \ No newline at end of file diff --git a/lib/core/interceptor/dio_http_error_interceptor.dart b/lib/core/interceptor/dio_http_error_interceptor.dart index f7f5670..0283770 100644 --- a/lib/core/interceptor/dio_http_error_interceptor.dart +++ b/lib/core/interceptor/dio_http_error_interceptor.dart @@ -47,7 +47,7 @@ class DioHttpErrorInterceptor extends Interceptor { errorMessages.putIfAbsent(entry.key, () => entry.value.toString()); } } - return handler.reject( + handler.reject( DioError( error: errorMessages, requestOptions: err.requestOptions, diff --git a/lib/core/type/types.dart b/lib/core/type/types.dart index 8130996..3ed65fa 100644 --- a/lib/core/type/types.dart +++ b/lib/core/type/types.dart @@ -1,2 +1,8 @@ typedef JSON = Map; typedef PaperlessValidationErrors = Map; +typedef PaperlessLocalizedErrorMessage = String; + +extension ValidationErrorsUtils on PaperlessValidationErrors { + bool get hasFieldUnspecificError => containsKey("non_field_errors"); + String? get fieldUnspecificError => this['non_field_errors']; +} diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index 6e52bc4..1f0a2e5 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -22,6 +22,7 @@ class DocumentUploadPreparationPage extends StatefulWidget { final Uint8List fileBytes; final String? title; final String? filename; + final String? fileExtension; final void Function(DocumentModel)? onSuccessfullyConsumed; const DocumentUploadPreparationPage({ @@ -30,6 +31,7 @@ class DocumentUploadPreparationPage extends StatefulWidget { this.title, this.filename, this.onSuccessfullyConsumed, + this.fileExtension, }) : super(key: key); @override @@ -119,7 +121,7 @@ class _DocumentUploadPreparationPageState name: fkFileName, decoration: InputDecoration( labelText: S.of(context).documentUploadFileNameLabel, - suffixText: ".pdf", + suffixText: widget.fileExtension, suffixIcon: IconButton( icon: const Icon(Icons.clear), onPressed: () => _formKey.currentState?.fields[fkFileName] diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 09ab8e8..ad4b9bc 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -1,13 +1,13 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'dart:developer'; + +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -class DocumentsCubit extends Cubit { +class DocumentsCubit extends HydratedCubit { final PaperlessDocumentsApi _api; - DocumentsCubit(this._api) : super(DocumentsState.initial); + DocumentsCubit(this._api) : super(const DocumentsState()); Future bulkRemove(List documents) async { await _api.bulkAction( @@ -40,42 +40,85 @@ class DocumentsCubit extends Cubit { } Future load() async { - final result = await _api.find(state.filter); - emit(DocumentsState( - isLoaded: true, - value: [...state.value, result], - filter: state.filter, - )); + emit(state.copyWith(isLoading: true)); + try { + final result = await _api.find(state.filter); + emit(state.copyWith( + isLoading: false, + hasLoaded: true, + value: [...state.value, result], + )); + } catch (err) { + emit(state.copyWith(isLoading: false)); + rethrow; + } } Future reload() async { - if (state.currentPageNumber >= 5) { - return _bulkReloadDocuments(); + emit(state.copyWith(isLoading: true)); + try { + if (state.currentPageNumber >= 5) { + return _bulkReloadDocuments(); + } + var newPages = >[]; + for (final page in state.value) { + final result = + await _api.find(state.filter.copyWith(page: page.pageKey)); + newPages.add(result); + } + emit(DocumentsState( + hasLoaded: true, + value: newPages, + filter: state.filter, + isLoading: false, + )); + } catch (err) { + emit(state.copyWith(isLoading: false)); + rethrow; } - var newPages = []; - for (final page in state.value) { - final result = await _api.find(state.filter.copyWith(page: page.pageKey)); - newPages.add(result); - } - emit(DocumentsState(isLoaded: true, value: newPages, filter: state.filter)); } Future _bulkReloadDocuments() async { - final result = await _api - .find(state.filter.copyWith(page: 1, pageSize: state.documents.length)); - emit(DocumentsState(isLoaded: true, value: [result], filter: state.filter)); + emit(state.copyWith(isLoading: true)); + try { + final result = await _api.find( + state.filter.copyWith( + page: 1, + pageSize: state.documents.length, + ), + ); + emit(DocumentsState( + hasLoaded: true, + value: [result], + filter: state.filter, + isLoading: false, + )); + } catch (err) { + emit(state.copyWith(isLoading: false)); + rethrow; + } } Future loadMore() async { if (state.isLastPageLoaded) { return; } + emit(state.copyWith(isLoading: true)); final newFilter = state.filter.copyWith(page: state.filter.page + 1); - final result = await _api.find(newFilter); - emit( - DocumentsState( - isLoaded: true, value: [...state.value, result], filter: newFilter), - ); + try { + final result = await _api.find(newFilter); + emit( + DocumentsState( + hasLoaded: true, + value: [...state.value, result], + filter: newFilter, + isLoading: false, + ), + ); + } catch (err) { + emit(state.copyWith(isLoading: false)); + rethrow; + } } /// @@ -84,8 +127,21 @@ class DocumentsCubit extends Cubit { Future updateFilter({ final DocumentFilter filter = DocumentFilter.initial, }) async { - final result = await _api.find(filter.copyWith(page: 1)); - emit(DocumentsState(filter: filter, value: [result], isLoaded: true)); + try { + emit(state.copyWith(isLoading: true)); + final result = await _api.find(filter.copyWith(page: 1)); + emit( + DocumentsState( + filter: filter, + value: [result], + hasLoaded: true, + isLoading: false, + ), + ); + } catch (err) { + emit(state.copyWith(isLoading: false)); + rethrow; + } } Future resetFilter() { @@ -125,6 +181,17 @@ class DocumentsCubit extends Cubit { } void reset() { - emit(DocumentsState.initial); + emit(const DocumentsState()); + } + + @override + DocumentsState? fromJson(Map json) { + log(json['filter'].toString()); + return DocumentsState.fromJson(json); + } + + @override + Map? toJson(DocumentsState state) { + return state.toJson(); } } diff --git a/lib/features/documents/bloc/documents_state.dart b/lib/features/documents/bloc/documents_state.dart index 2b0e19a..e3f0f55 100644 --- a/lib/features/documents/bloc/documents_state.dart +++ b/lib/features/documents/bloc/documents_state.dart @@ -1,30 +1,27 @@ +import 'dart:developer'; + import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; @JsonSerializable() class DocumentsState extends Equatable { - final bool isLoaded; + final bool isLoading; + final bool hasLoaded; final DocumentFilter filter; - final List value; + final List> value; @JsonKey(ignore: true) final List selection; const DocumentsState({ - required this.isLoaded, - required this.value, - required this.filter, + this.hasLoaded = false, + this.isLoading = false, + this.value = const [], + this.filter = const DocumentFilter(), this.selection = const [], }); - static const DocumentsState initial = DocumentsState( - isLoaded: false, - value: [], - filter: DocumentFilter.initial, - selection: [], - ); - int get currentPageNumber { return filter.page; } @@ -41,7 +38,7 @@ class DocumentsState extends Equatable { } bool get isLastPageLoaded { - if (!isLoaded) { + if (!hasLoaded) { return false; } if (value.isNotEmpty) { @@ -51,7 +48,7 @@ class DocumentsState extends Equatable { } int inferPageCount({required int pageSize}) { - if (!isLoaded) { + if (!hasLoaded) { return 100000; } if (value.isEmpty) { @@ -67,13 +64,15 @@ class DocumentsState extends Equatable { DocumentsState copyWith({ bool overwrite = false, - bool? isLoaded, - List? value, + bool? hasLoaded, + bool? isLoading, + List>? value, DocumentFilter? filter, List? selection, }) { return DocumentsState( - isLoaded: isLoaded ?? this.isLoaded, + hasLoaded: hasLoaded ?? this.hasLoaded, + isLoading: isLoading ?? this.isLoading, value: value ?? this.value, filter: filter ?? this.filter, selection: selection ?? this.selection, @@ -81,5 +80,28 @@ class DocumentsState extends Equatable { } @override - List get props => [isLoaded, filter, value, selection]; + List get props => [hasLoaded, filter, value, selection, isLoading]; + + Map toJson() { + final json = { + 'hasLoaded': hasLoaded, + 'isLoading': isLoading, + 'filter': filter.toJson(), + 'value': + value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(), + }; + return json; + } + + factory DocumentsState.fromJson(Map json) { + return DocumentsState( + hasLoaded: json['hasLoaded'], + isLoading: json['isLoading'], + value: (json['value'] as List) + .map((e) => + PagedSearchResult.fromJson(e, DocumentModelJsonConverter())) + .toList(), + filter: DocumentFilter.fromJson(json['filter']), + ); + } } diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 2cd466e..d91c0d0 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -35,6 +35,7 @@ class _DocumentsPageState extends State { final _pagingController = PagingController( firstPageKey: 1, ); + final _refreshIndicatorKey = GlobalKey(); @override void initState() { @@ -78,8 +79,8 @@ class _DocumentsPageState extends State { builder: (context, state) { final appliedFiltersCount = state.filter.appliedFiltersCount; return Badge.count( - alignment: const AlignmentDirectional(44, - -4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd + //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd + alignment: const AlignmentDirectional(44, -4), isLabelVisible: appliedFiltersCount > 0, count: state.filter.appliedFiltersCount, backgroundColor: Colors.red, @@ -177,7 +178,7 @@ class _DocumentsPageState extends State { break; } - if (state.isLoaded && state.documents.isEmpty) { + if (state.hasLoaded && state.documents.isEmpty) { child = SliverToBoxAdapter( child: DocumentsEmptyState( state: state, @@ -190,6 +191,7 @@ class _DocumentsPageState extends State { } return RefreshIndicator( + key: _refreshIndicatorKey, onRefresh: _onRefresh, notificationPredicate: (_) => isConnected, child: CustomScrollView( @@ -369,10 +371,10 @@ class _DocumentsPageState extends State { Future _onRefresh() async { try { - context.read().updateCurrentFilter( + await context.read().updateCurrentFilter( (filter) => filter.copyWith(page: 1), ); - context.read().reload(); + await context.read().reload(); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } diff --git a/lib/features/documents/view/widgets/search/text_query_form_field.dart b/lib/features/documents/view/widgets/search/text_query_form_field.dart index ace67a0..420e529 100644 --- a/lib/features/documents/view/widgets/search/text_query_form_field.dart +++ b/lib/features/documents/view/widgets/search/text_query_form_field.dart @@ -28,10 +28,13 @@ class TextQueryFormField extends StatelessWidget { prefixIcon: const Icon(Icons.search_outlined), labelText: _buildLabelText(context, field.value!.queryType), suffixIcon: PopupMenuButton( - enabled: !onlyExtendedQueryAllowed, - color: onlyExtendedQueryAllowed - ? Theme.of(context).disabledColor + icon: onlyExtendedQueryAllowed + ? Icon( + Icons.more_vert, + color: Theme.of(context).disabledColor, + ) : null, + enabled: !onlyExtendedQueryAllowed, itemBuilder: (context) => [ PopupMenuItem( child: ListTile( @@ -59,7 +62,6 @@ class TextQueryFormField extends StatelessWidget { onSelected: (selection) { field.didChange(field.value?.copyWith(queryType: selection)); }, - child: const Icon(Icons.more_vert), ), ), onChanged: (value) { diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart index 07b7431..0ee35de 100644 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -36,8 +38,15 @@ class _DocumentsPageAppBarState extends State { return BlocBuilder( builder: (context, documentsState) { final hasSelection = documentsState.selection.isNotEmpty; + // final PreferredSize? loadingWidget = documentsState.isLoading + // ? const PreferredSize( + // child: LinearProgressIndicator(), + // preferredSize: Size.fromHeight(4.0), + // ) + // : null; if (hasSelection) { return SliverAppBar( + // bottom: loadingWidget, expandedHeight: kToolbarHeight + flexibleAreaHeight, snap: true, floating: true, @@ -62,6 +71,7 @@ class _DocumentsPageAppBarState extends State { ); } else { return SliverAppBar( + // bottom: loadingWidget, expandedHeight: kToolbarHeight + flexibleAreaHeight, snap: true, floating: true, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 5f5562e..31bc5a2 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -1,10 +1,19 @@ +import 'dart:developer'; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +import 'package:paperless_mobile/core/global/constants.dart'; +import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; +import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart'; @@ -13,8 +22,12 @@ import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; +import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -31,6 +44,126 @@ class _HomePageState extends State { void initState() { super.initState(); _initializeData(context); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _listenForReceivedFiles(); + }); + } + + void _listenForReceivedFiles() async { + if (ShareIntentQueue.instance.hasUnhandledFiles) { + Fluttertoast.showToast(msg: "Sync: Has unhandled files!"); + await _handleReceivedFile(ShareIntentQueue.instance.pop()!); + Fluttertoast.showToast(msg: "Sync: File handled!"); + } + ShareIntentQueue.instance.addListener(() async { + final queue = ShareIntentQueue.instance; + while (queue.hasUnhandledFiles) { + Fluttertoast.showToast(msg: "Async: Has unhandled files!"); + final file = queue.pop()!; + await _handleReceivedFile(file); + Fluttertoast.showToast(msg: "Async: File handled!"); + } + }); + } + + bool _isFileTypeSupported(SharedMediaFile file) { + return supportedFileExtensions.contains( + file.path.split('.').last.toLowerCase(), + ); + } + + Future _handleReceivedFile(SharedMediaFile file) async { + final isGranted = await askForPermission(Permission.storage); + + if (!isGranted) { + return; + } + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Received File."), + content: Column( + children: [ + Text("Path: ${file.path}"), + Text("Type: ${file.type.name}"), + Text("Exists: ${File(file.path).existsSync()}"), + FutureBuilder( + future: Permission.storage.isGranted, + builder: (context, snapshot) => + Text("Has storage permission: ${snapshot.data}"), + ) + ], + ), + )); + SharedMediaFile mediaFile; + if (Platform.isIOS) { + // Workaround for file not found on iOS: https://stackoverflow.com/a/72813212 + mediaFile = SharedMediaFile( + file.path.replaceAll('file://', ''), + file.thumbnail, + file.duration, + file.type, + ); + } else { + mediaFile = file; + } + + if (!_isFileTypeSupported(mediaFile)) { + Fluttertoast.showToast( + msg: translateError(context, ErrorCode.unsupportedFileFormat), + ); + if (Platform.isAndroid) { + // As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines. + await SystemNavigator.pop(); + } + return; + } + final filename = extractFilenameFromPath(mediaFile.path); + + try { + if (File(mediaFile.path).existsSync()) { + final bytes = File(mediaFile.path).readAsBytesSync(); + final success = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: DocumentUploadCubit( + localVault: context.read(), + documentApi: context.read(), + tagRepository: context.read(), + correspondentRepository: context.read(), + documentTypeRepository: context.read(), + ), + child: DocumentUploadPreparationPage( + fileBytes: bytes, + filename: filename, + ), + ), + ), + ) ?? + false; + if (success) { + await Fluttertoast.showToast( + msg: S.of(context).documentUploadSuccessText, + ); + SystemNavigator.pop(); + } + } + } catch (e, stackTrace) { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: Column( + children: [ + Text( + e.toString(), + ), + Text(stackTrace.toString()), + ], + ), + ), + ); + } } @override diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 1f73517..c3aa3c2 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -22,8 +22,7 @@ class InboxPage extends StatefulWidget { } class _InboxPageState extends State { - final GlobalKey _emptyStateRefreshIndicatorKey = - GlobalKey(); + final _emptyStateRefreshIndicatorKey = GlobalKey(); @override void initState() { diff --git a/lib/features/login/bloc/authentication_cubit.dart b/lib/features/login/bloc/authentication_cubit.dart index 9508a1a..bd7a20a 100644 --- a/lib/features/login/bloc/authentication_cubit.dart +++ b/lib/features/login/bloc/authentication_cubit.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart'; @@ -34,7 +35,6 @@ class AuthenticationCubit extends Cubit username: credentials.username!, password: credentials.password!, ); - _dioWrapper.updateSettings( baseUrl: serverUrl, clientCertificate: clientCertificate, diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 0820e60..f49baee 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -2,16 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; -import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/server_connection_page.dart'; -import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'widgets/never_scrollable_scroll_behavior.dart'; -import 'widgets/server_login_page.dart'; +import 'widgets/login_pages/server_login_page.dart'; class LoginPage extends StatefulWidget { const LoginPage({Key? key}) : super(key: key); @@ -29,14 +30,6 @@ class _LoginPageState extends State { Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, // appBar: AppBar( - // title: Text(S.of(context).loginPageTitle), - // bottom: _isLoginLoading - // ? const PreferredSize( - // preferredSize: Size(double.infinity, 4), - // child: LinearProgressIndicator(), - // ) - // : null, - // ), body: FormBuilder( key: _formKey, child: PageView( @@ -47,8 +40,9 @@ class _LoginPageState extends State { formBuilderKey: _formKey, onContinue: () { _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut); + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); }, ), ServerLoginPage( @@ -58,58 +52,10 @@ class _LoginPageState extends State { ], ), ), - // Padding( - // padding: const EdgeInsets.all(8.0), - // child: FormBuilder( - // key: _formKey, - // child: ListView( - // children: [ - // const ServerAddressFormField().padded(), - // const UserCredentialsFormField(), - // Align( - // alignment: Alignment.centerLeft, - // child: Padding( - // padding: const EdgeInsets.only(top: 16.0), - // child: Text( - // S.of(context).loginPageAdvancedLabel, - // style: Theme.of(context).textTheme.bodyLarge, - // ).padded(), - // ), - // ), - // const ClientCertificateFormField(), - // LayoutBuilder(builder: (context, constraints) { - // return Padding( - // padding: const EdgeInsets.all(8.0), - // child: SizedBox( - // width: constraints.maxWidth, - // child: _buildLoginButton(), - // ), - // ); - // }), - // ], - // ), - // ), - // ), ); } - Widget _buildLoginButton() { - return ElevatedButton( - key: const ValueKey('login-login-button'), - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll( - Theme.of(context).colorScheme.primaryContainer, - ), - elevation: const MaterialStatePropertyAll(0), - ), - onPressed: _login, - child: Text( - S.of(context).loginPageLoginButtonLabel, - ), - ); - } - - void _login() async { + Future _login() async { FocusScope.of(context).unfocus(); if (_formKey.currentState?.saveAndValidate() ?? false) { final form = _formKey.currentState!.value; @@ -122,11 +68,15 @@ class _LoginPageState extends State { ); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); - } on Map catch (error, stackTrace) { - showGenericError(context, error.values.first, stackTrace); + } on PaperlessValidationErrors catch (error, stackTrace) { + if (error.hasFieldUnspecificError) { + showLocalizedError(context, error.fieldUnspecificError!); + } else { + showGenericError(context, error.values.first, stackTrace); + } } catch (unknownError, stackTrace) { showGenericError(context, unknownError.toString(), stackTrace); - } finally {} + } } } } diff --git a/lib/features/login/view/widgets/client_certificate_form_field.dart b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart similarity index 96% rename from lib/features/login/view/widgets/client_certificate_form_field.dart rename to lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart index 6547c03..408577b 100644 --- a/lib/features/login/view/widgets/client_certificate_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/client_certificate_form_field.dart @@ -5,8 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:paperless_mobile/features/login/view/widgets/password_text_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/util.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'obscured_input_text_form_field.dart'; class ClientCertificateFormField extends StatefulWidget { static const fkClientCertificate = 'clientCertificate'; @@ -24,7 +27,6 @@ class ClientCertificateFormField extends StatefulWidget { class _ClientCertificateFormFieldState extends State { - RestorableString? _selectedFilePath; File? _selectedFile; @override Widget build(BuildContext context) { @@ -105,7 +107,9 @@ class _ClientCertificateFormFieldState } Future _onSelectFile(FormFieldState field) async { - FilePickerResult? result = await FilePicker.platform.pickFiles(); + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: false, + ); if (result != null && result.files.single.path != null) { File file = File(result.files.single.path!); setState(() { diff --git a/lib/features/login/view/widgets/password_text_field.dart b/lib/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart similarity index 100% rename from lib/features/login/view/widgets/password_text_field.dart rename to lib/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart diff --git a/lib/features/login/view/widgets/server_address_form_field.dart b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart similarity index 89% rename from lib/features/login/view/widgets/server_address_form_field.dart rename to lib/features/login/view/widgets/form_fields/server_address_form_field.dart index db34df7..ee5b0d7 100644 --- a/lib/features/login/view/widgets/server_address_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; -import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:provider/provider.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; diff --git a/lib/features/login/view/widgets/user_credentials_form_field.dart b/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart similarity index 97% rename from lib/features/login/view/widgets/user_credentials_form_field.dart rename to lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart index a4d1596..87916ad 100644 --- a/lib/features/login/view/widgets/user_credentials_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/user_credentials_form_field.dart @@ -3,7 +3,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/user_credentials.model.dart'; -import 'package:paperless_mobile/features/login/view/widgets/password_text_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class UserCredentialsFormField extends StatefulWidget { diff --git a/lib/features/login/view/widgets/server_connection_page.dart b/lib/features/login/view/widgets/login_pages/server_connection_page.dart similarity index 84% rename from lib/features/login/view/widgets/server_connection_page.dart rename to lib/features/login/view/widgets/login_pages/server_connection_page.dart index 11bd994..d985ced 100644 --- a/lib/features/login/view/widgets/server_connection_page.dart +++ b/lib/features/login/view/widgets/login_pages/server_connection_page.dart @@ -4,8 +4,8 @@ import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/login/model/reachability_status.dart'; -import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:provider/provider.dart'; @@ -28,7 +28,6 @@ class _ServerConnectionPageState extends State { @override Widget build(BuildContext context) { - final logoHeight = MediaQuery.of(context).size.width / 2; return Scaffold( appBar: AppBar( title: Text(S.of(context).loginPageTitle), @@ -52,7 +51,7 @@ class _ServerConnectionPageState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ FilledButton( - child: Text("Continue"), + child: Text(S.of(context).loginPageContinueLabel), onPressed: _reachabilityStatus == ReachabilityStatus.reachable ? widget.onContinue : null, @@ -84,31 +83,33 @@ class _ServerConnectionPageState extends State { case ReachabilityStatus.reachable: return _buildIconText( Icons.done, - "Connection established.", + S.of(context).loginPageReachabilitySuccessText, Colors.green, ); case ReachabilityStatus.notReachable: return _buildIconText( Icons.close, - "Could not establish a connection to the server.", + S.of(context).loginPageReachabilityNotReachableText, errorColor, ); case ReachabilityStatus.unknownHost: return _buildIconText( Icons.close, - "Host could not be resolved.", + S.of(context).loginPageReachabilityUnresolvedHostText, errorColor, ); case ReachabilityStatus.missingClientCertificate: return _buildIconText( Icons.close, - "A client certificate was expected but not sent. Please provide a certificate.", + S.of(context).loginPageReachabilityMissingClientCertificateText, errorColor, ); case ReachabilityStatus.invalidClientCertificateConfiguration: return _buildIconText( Icons.close, - "Incorrect or missing client certificate passphrase.", + S + .of(context) + .loginPageReachabilityInvalidClientCertificateConfigurationText, errorColor, ); } diff --git a/lib/features/login/view/widgets/login_pages/server_login_page.dart b/lib/features/login/view/widgets/login_pages/server_login_page.dart new file mode 100644 index 0000000..e5328b5 --- /dev/null +++ b/lib/features/login/view/widgets/login_pages/server_login_page.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class ServerLoginPage extends StatefulWidget { + final Future Function() onDone; + final GlobalKey formBuilderKey; + const ServerLoginPage({ + super.key, + required this.onDone, + required this.formBuilderKey, + }); + + @override + State createState() => _ServerLoginPageState(); +} + +class _ServerLoginPageState extends State { + bool _isLoginLoading = false; + @override + Widget build(BuildContext context) { + final serverAddress = (widget.formBuilderKey.currentState + ?.getRawValue(ServerAddressFormField.fkServerAddress) + as String?) + ?.replaceAll(RegExp(r'https?://'), '') ?? + ''; + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).loginPageSignInTitle), + bottom: _isLoginLoading + ? const PreferredSize( + preferredSize: Size.fromHeight(4.0), + child: LinearProgressIndicator(), + ) + : null, + ), + body: ListView( + children: [ + Text(S.of(context).loginPageSignInToPrefixText(serverAddress)) + .padded(), + const UserCredentialsFormField(), + ], + ), + bottomNavigationBar: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilledButton( + onPressed: () async { + setState(() => _isLoginLoading = true); + await widget.onDone(); + setState(() => _isLoginLoading = false); + }, + child: Text(S.of(context).loginPageSignInButtonLabel), + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/login/view/widgets/server_login_page.dart b/lib/features/login/view/widgets/server_login_page.dart deleted file mode 100644 index 2459dc2..0000000 --- a/lib/features/login/view/widgets/server_login_page.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart'; - -class ServerLoginPage extends StatefulWidget { - final VoidCallback onDone; - final GlobalKey formBuilderKey; - const ServerLoginPage({ - super.key, - required this.onDone, - required this.formBuilderKey, - }); - - @override - State createState() => _ServerLoginPageState(); -} - -class _ServerLoginPageState extends State { - @override - Widget build(BuildContext context) { - final serverAddress = (widget.formBuilderKey.currentState - ?.getRawValue(ServerAddressFormField.fkServerAddress) as String?) - ?.replaceAll(RegExp(r'https?://'), ''); - return Scaffold( - appBar: AppBar( - title: Text("Sign In"), - ), - body: ListView( - children: [ - Text("Sign in to $serverAddress").padded(), - UserCredentialsFormField(), - ], - ), - bottomNavigationBar: BottomAppBar( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilledButton( - onPressed: widget.onDone, - child: Text("Sign In"), - ) - ], - ), - ), - ); - } -} diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 1b222ff..7435526 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -26,8 +26,8 @@ import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; +import 'package:path/path.dart' as p; import 'package:permission_handler/permission_handler.dart'; -import 'package:provider/provider.dart'; class ScannerPage extends StatefulWidget { const ScannerPage({Key? key}) : super(key: key); @@ -70,8 +70,10 @@ class _ScannerPageState extends State ? () => Navigator.of(context).push( MaterialPageRoute( builder: (context) => DocumentView( - documentBytes: - _buildDocumentFromImageFiles(state).save(), + documentBytes: _assembleFileBytes( + state, + forcePdf: true, + ).then((file) => file.bytes), ), ), ) @@ -106,7 +108,10 @@ class _ScannerPageState extends State } void _openDocumentScanner(BuildContext context) async { - await _requestCameraPermissions(); + final isGranted = await askForPermission(Permission.camera); + if (!isGranted) { + return; + } final file = await FileService.allocateTemporaryFile( PaperlessDirectoryType.scans, extension: 'jpeg', @@ -130,10 +135,9 @@ class _ScannerPageState extends State } void _onPrepareDocumentUpload(BuildContext context) async { - final doc = _buildDocumentFromImageFiles( + final file = await _assembleFileBytes( context.read().state, ); - final bytes = await doc.save(); final uploaded = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => LabelRepositoriesProvider( @@ -148,7 +152,8 @@ class _ScannerPageState extends State tagRepository: context.read>(), ), child: DocumentUploadPreparationPage( - fileBytes: bytes, + fileBytes: file.bytes, + fileExtension: file.extension, ), ), ), @@ -229,24 +234,17 @@ class _ScannerPageState extends State } } - Future _requestCameraPermissions() async { - final hasPermission = await Permission.camera.isGranted; - if (!hasPermission) { - await Permission.camera.request(); - } - } - void _onUploadFromFilesystem() async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: supportedFileExtensions, withData: true, + allowMultiple: false, ); if (result?.files.single.path != null) { File file = File(result!.files.single.path!); - if (!supportedFileExtensions.contains( - file.path.split('.').last.toLowerCase(), - )) { + if (!supportedFileExtensions + .contains(file.path.split('.').last.toLowerCase())) { showErrorMessage( context, const PaperlessServerException(ErrorCode.unsupportedFileFormat), @@ -254,14 +252,7 @@ class _ScannerPageState extends State return; } final filename = extractFilenameFromPath(file.path); - final mimeType = lookupMimeType(file.path) ?? ''; - late Uint8List fileBytes; - if (mimeType.startsWith('image')) { - fileBytes = await _buildDocumentFromImageFiles([file]).save(); - } else { - // pdf - fileBytes = file.readAsBytesSync(); - } + final extension = p.extension(file.path); Navigator.of(context).push( MaterialPageRoute( builder: (_) => LabelRepositoriesProvider( @@ -276,8 +267,9 @@ class _ScannerPageState extends State tagRepository: context.read>(), ), child: DocumentUploadPreparationPage( - fileBytes: fileBytes, + fileBytes: file.readAsBytesSync(), filename: filename, + fileExtension: extension, ), ), ), @@ -286,18 +278,38 @@ class _ScannerPageState extends State } } - pw.Document _buildDocumentFromImageFiles(List files) { + /// + /// Returns the file bytes of either a single file or multiple images concatenated into a single pdf. + /// + Future _assembleFileBytes( + final List files, { + bool forcePdf = false, + }) async { + assert(files.isNotEmpty); + if (files.length == 1 && !forcePdf) { + final ext = p.extension(files.first.path); + return AssembledFile(ext, files.first.readAsBytesSync()); + } final doc = pw.Document(); for (final file in files) { final img = pw.MemoryImage(file.readAsBytesSync()); doc.addPage( pw.Page( - pageFormat: - PdfPageFormat(img.width!.toDouble(), img.height!.toDouble()), + pageFormat: PdfPageFormat( + img.width!.toDouble(), + img.height!.toDouble(), + ), build: (context) => pw.Image(img), ), ); } - return doc; + return AssembledFile('.pdf', await doc.save()); } } + +class AssembledFile { + final String extension; + final Uint8List bytes; + + AssembledFile(this.extension, this.bytes); +} diff --git a/lib/features/scan/view/widgets/scanner.dart b/lib/features/scan/view/widgets/scanner.dart index 786df64..6b6e76a 100644 --- a/lib/features/scan/view/widgets/scanner.dart +++ b/lib/features/scan/view/widgets/scanner.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:paperless_mobile/util.dart'; import 'package:permission_handler/permission_handler.dart'; typedef OnImageScannedCallback = void Function(File); @@ -23,14 +24,13 @@ class _ScannerWidgetState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Scan document")), - body: FutureBuilder( - future: Permission.camera.request(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { + body: FutureBuilder( + future: askForPermission(Permission.camera), + builder: (BuildContext context, AsyncSnapshot snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } - if (snapshot.data!.isGranted) { + if (snapshot.data!) { return Container(); } return const Center( diff --git a/lib/features/sharing/share_intent_queue.dart b/lib/features/sharing/share_intent_queue.dart new file mode 100644 index 0000000..4ef0df2 --- /dev/null +++ b/lib/features/sharing/share_intent_queue.dart @@ -0,0 +1,36 @@ +import 'dart:collection'; +import 'dart:developer'; + +import 'package:flutter/widgets.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:rxdart/rxdart.dart'; + +class ShareIntentQueue extends ChangeNotifier { + final Queue _queue = Queue(); + + ShareIntentQueue._(); + + static final instance = ShareIntentQueue._(); + + void add(SharedMediaFile file) { + _queue.add(file); + notifyListeners(); + } + + void addAll(Iterable files) { + _queue.addAll(files); + notifyListeners(); + } + + SharedMediaFile? pop() { + if (hasUnhandledFiles) { + return _queue.removeFirst(); + // Don't notify listeners, only when new item is added. + } else { + return null; + } + } + + bool get hasUnhandledFiles => _queue.isNotEmpty; +} diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 8afb7c9..ea77cda 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -428,6 +428,8 @@ "@loginPageClientCertificateSettingLabel": {}, "loginPageClientCertificateSettingSelectFileText": "Vybrat soubor...", "@loginPageClientCertificateSettingSelectFileText": {}, + "loginPageContinueLabel": "Continue", + "@loginPageContinueLabel": {}, "loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Chybná nebo chybějící heslová fráze certifikátu.", "@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {}, "loginPageLoginButtonLabel": "Připojit", @@ -436,12 +438,32 @@ "@loginPagePasswordFieldLabel": {}, "loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.", "@loginPagePasswordValidatorMessageText": {}, + "loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.", + "@loginPageReachabilityInvalidClientCertificateConfigurationText": {}, + "loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.", + "@loginPageReachabilityMissingClientCertificateText": {}, + "loginPageReachabilityNotReachableText": "Could not establish a connection to the server.", + "@loginPageReachabilityNotReachableText": {}, + "loginPageReachabilitySuccessText": "Connection successfully established.", + "@loginPageReachabilitySuccessText": {}, + "loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.", + "@loginPageReachabilityUnresolvedHostText": {}, "loginPageServerUrlFieldLabel": "'Adresa serveru", "@loginPageServerUrlFieldLabel": {}, "loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.", "@loginPageServerUrlValidatorMessageInvalidAddressText": {}, "loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.", "@loginPageServerUrlValidatorMessageRequiredText": {}, + "loginPageSignInButtonLabel": "Sign In", + "@loginPageSignInButtonLabel": {}, + "loginPageSignInTitle": "Sign In", + "@loginPageSignInTitle": {}, + "loginPageSignInToPrefixText": "Sign in to {serverAddress}", + "@loginPageSignInToPrefixText": { + "placeholders": { + "serverAddress": {} + } + }, "loginPageTitle": "Propojit s Paperless", "@loginPageTitle": {}, "loginPageUsernameLabel": "Jméno uživatele", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 7a11762..d5fc99d 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -428,6 +428,8 @@ "@loginPageClientCertificateSettingLabel": {}, "loginPageClientCertificateSettingSelectFileText": "Datei auswählen...", "@loginPageClientCertificateSettingSelectFileText": {}, + "loginPageContinueLabel": "Fortfahren", + "@loginPageContinueLabel": {}, "loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Falsche oder fehlende Zertifikatspassphrase.", "@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {}, "loginPageLoginButtonLabel": "Verbinden", @@ -436,12 +438,32 @@ "@loginPagePasswordFieldLabel": {}, "loginPagePasswordValidatorMessageText": "Passwort darf nicht leer sein.", "@loginPagePasswordValidatorMessageText": {}, + "loginPageReachabilityInvalidClientCertificateConfigurationText": "Inkorrekte oder fehlende Zertifikatspassphrase.", + "@loginPageReachabilityInvalidClientCertificateConfigurationText": {}, + "loginPageReachabilityMissingClientCertificateText": "Ein Client-Zertifikat wurde erwartet aber nicht gesendet. Bitte stelle ein Zertifikat zur Verfügung.", + "@loginPageReachabilityMissingClientCertificateText": {}, + "loginPageReachabilityNotReachableText": "Es konnte keine Verbindung zum Server hergestellt werden.", + "@loginPageReachabilityNotReachableText": {}, + "loginPageReachabilitySuccessText": "Verbindung erfolgreich hergestellt.", + "@loginPageReachabilitySuccessText": {}, + "loginPageReachabilityUnresolvedHostText": "Der Host konnte nicht aufgelöst werden. Bitte überprüfe die Server-Adresse.", + "@loginPageReachabilityUnresolvedHostText": {}, "loginPageServerUrlFieldLabel": "Server-Adresse", "@loginPageServerUrlFieldLabel": {}, "loginPageServerUrlValidatorMessageInvalidAddressText": "Ungültige Adresse.", "@loginPageServerUrlValidatorMessageInvalidAddressText": {}, "loginPageServerUrlValidatorMessageRequiredText": "Server-Addresse darf nicht leer sein.", "@loginPageServerUrlValidatorMessageRequiredText": {}, + "loginPageSignInButtonLabel": "Anmelden", + "@loginPageSignInButtonLabel": {}, + "loginPageSignInTitle": "Anmelden", + "@loginPageSignInTitle": {}, + "loginPageSignInToPrefixText": "Bei {serverAddress} anmelden", + "@loginPageSignInToPrefixText": { + "placeholders": { + "serverAddress": {} + } + }, "loginPageTitle": "Mit Paperless verbinden", "@loginPageTitle": {}, "loginPageUsernameLabel": "Nutzername", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index abb28ca..8538c4a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -428,6 +428,8 @@ "@loginPageClientCertificateSettingLabel": {}, "loginPageClientCertificateSettingSelectFileText": "Select file...", "@loginPageClientCertificateSettingSelectFileText": {}, + "loginPageContinueLabel": "Continue", + "@loginPageContinueLabel": {}, "loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Incorrect or missing certificate passphrase.", "@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {}, "loginPageLoginButtonLabel": "Connect", @@ -436,12 +438,32 @@ "@loginPagePasswordFieldLabel": {}, "loginPagePasswordValidatorMessageText": "Password must not be empty.", "@loginPagePasswordValidatorMessageText": {}, + "loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.", + "@loginPageReachabilityInvalidClientCertificateConfigurationText": {}, + "loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.", + "@loginPageReachabilityMissingClientCertificateText": {}, + "loginPageReachabilityNotReachableText": "Could not establish a connection to the server.", + "@loginPageReachabilityNotReachableText": {}, + "loginPageReachabilitySuccessText": "Connection successfully established.", + "@loginPageReachabilitySuccessText": {}, + "loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.", + "@loginPageReachabilityUnresolvedHostText": {}, "loginPageServerUrlFieldLabel": "Server Address", "@loginPageServerUrlFieldLabel": {}, "loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.", "@loginPageServerUrlValidatorMessageInvalidAddressText": {}, "loginPageServerUrlValidatorMessageRequiredText": "Server address must not be empty.", "@loginPageServerUrlValidatorMessageRequiredText": {}, + "loginPageSignInButtonLabel": "Sign In", + "@loginPageSignInButtonLabel": {}, + "loginPageSignInTitle": "Sign In", + "@loginPageSignInTitle": {}, + "loginPageSignInToPrefixText": "Sign in to {serverAddress}", + "@loginPageSignInToPrefixText": { + "placeholders": { + "serverAddress": {} + } + }, "loginPageTitle": "Connect to Paperless", "@loginPageTitle": {}, "loginPageUsernameLabel": "Username", diff --git a/lib/main.dart b/lib/main.dart index 96fbd38..99f5728 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,10 @@ -import 'dart:io'; - import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -18,10 +14,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; -import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; -import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart'; @@ -35,8 +29,6 @@ import 'package:paperless_mobile/core/service/dio_file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; -import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; -import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; @@ -45,8 +37,8 @@ import 'package:paperless_mobile/features/login/services/authentication_service. import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; +import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:provider/provider.dart'; @@ -136,7 +128,6 @@ void main() async { //Update language header in interceptor on language change. appSettingsCubit.stream.listen((event) => languageHeaderInterceptor .preferredLocaleSubtag = event.preferredLocaleSubtag); - runApp( MultiProvider( providers: [ @@ -292,67 +283,6 @@ class AuthenticationWrapper extends StatefulWidget { } class _AuthenticationWrapperState extends State { - bool isFileTypeSupported(SharedMediaFile file) { - return supportedFileExtensions.contains( - file.path.split('.').last.toLowerCase(), - ); - } - - void handleReceivedFiles(List files) async { - if (files.isEmpty) { - return; - } - late final SharedMediaFile file; - if (Platform.isIOS) { - // Workaround for file not found on iOS: https://stackoverflow.com/a/72813212 - file = SharedMediaFile( - files.first.path.replaceAll('file://', ''), - files.first.thumbnail, - files.first.duration, - files.first.type, - ); - } else { - file = files.first; - } - - if (!isFileTypeSupported(file)) { - Fluttertoast.showToast( - msg: translateError(context, ErrorCode.unsupportedFileFormat), - ); - if (Platform.isAndroid) { - // As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines. - await SystemNavigator.pop(); - } - return; - } - final filename = extractFilenameFromPath(file.path); - final bytes = File(file.path).readAsBytesSync(); - final success = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: DocumentUploadCubit( - localVault: context.read(), - documentApi: context.read(), - tagRepository: context.read(), - correspondentRepository: context.read(), - documentTypeRepository: context.read(), - ), - child: DocumentUploadPreparationPage( - fileBytes: bytes, - filename: filename, - ), - ), - ), - ); - if (success) { - Fluttertoast.showToast( - msg: S.of(context).documentUploadSuccessText, - ); - SystemNavigator.pop(); - } - } - @override void didChangeDependencies() { FlutterNativeSplash.remove(); @@ -364,18 +294,17 @@ class _AuthenticationWrapperState extends State { super.initState(); initializeDateFormatting(); // For sharing files coming from outside the app while the app is still opened - ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles); + ReceiveSharingIntent.getMediaStream() + .listen(ShareIntentQueue.instance.addAll); // For sharing files coming from outside the app while the app is closed - ReceiveSharingIntent.getInitialMedia().then(handleReceivedFiles); + ReceiveSharingIntent.getInitialMedia() + .then(ShareIntentQueue.instance.addAll); } @override Widget build(BuildContext context) { return SafeArea( top: true, - left: false, - right: false, - bottom: false, child: BlocConsumer( listener: (context, authState) { final bool showIntroSlider = diff --git a/lib/util.dart b/lib/util.dart index 3af3b6f..c6395d8 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:developer'; +import 'dart:io'; import 'dart:ui'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -11,6 +13,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/service/github_issue_service.dart'; import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:permission_handler/permission_handler.dart'; final dateFormat = DateFormat("yyyy-MM-dd"); final GlobalKey rootScaffoldKey = GlobalKey(); @@ -90,6 +93,15 @@ void showGenericError( ); } +void showLocalizedError( + BuildContext context, + String localizedMessage, [ + StackTrace? stackTrace, +]) { + showSnackBar(context, localizedMessage); + log(localizedMessage, stackTrace: stackTrace); +} + void showErrorMessage( BuildContext context, PaperlessServerException error, [ @@ -156,3 +168,14 @@ Future loadImage(ImageProvider provider) { stream.addListener(listener); return completer.future; } + +Future askForPermission(Permission permission) async { + final status = await permission.request(); + log("Permission requested, new status is $status"); + // If user has permanently declined permission, open settings. + if (status == PermissionStatus.permanentlyDenied) { + await openAppSettings(); + } + + return status == PermissionStatus.granted; +} diff --git a/packages/paperless_api/lib/paperless_api.dart b/packages/paperless_api/lib/paperless_api.dart index 6272edc..1ef273c 100644 --- a/packages/paperless_api/lib/paperless_api.dart +++ b/packages/paperless_api/lib/paperless_api.dart @@ -2,3 +2,4 @@ library paperless_api; export 'src/models/models.dart'; export 'src/modules/modules.dart'; +export 'src/converters/converters.dart'; diff --git a/packages/paperless_api/lib/src/converters/converters.dart b/packages/paperless_api/lib/src/converters/converters.dart new file mode 100644 index 0000000..10a8c8e --- /dev/null +++ b/packages/paperless_api/lib/src/converters/converters.dart @@ -0,0 +1,3 @@ +export 'document_model_json_converter.dart'; +export 'similar_document_model_json_converter.dart'; +export 'date_range_query_json_converter.dart'; diff --git a/packages/paperless_api/lib/src/converters/date_range_query_json_converter.dart b/packages/paperless_api/lib/src/converters/date_range_query_json_converter.dart new file mode 100644 index 0000000..52a45ee --- /dev/null +++ b/packages/paperless_api/lib/src/converters/date_range_query_json_converter.dart @@ -0,0 +1,32 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/src/models/models.dart'; +import 'package:paperless_api/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart'; +import 'package:paperless_api/src/models/query_parameters/date_range_queries/relative_date_range_query.dart'; + +class DateRangeQueryJsonConverter + extends JsonConverter> { + const DateRangeQueryJsonConverter(); + @override + DateRangeQuery fromJson(Map json) { + final type = json['type']; + final data = json['data']; + switch (json['type'] as String) { + case 'UnsetDateRangeQuery': + return const UnsetDateRangeQuery(); + case 'AbsoluteDateRangeQuery': + return AbsoluteDateRangeQuery.fromJson(data); + case 'RelativeDateRangeQuery': + return RelativeDateRangeQuery.fromJson(data); + default: + throw Exception('Error parsing DateRangeQuery: Unknown type $type'); + } + } + + @override + Map toJson(DateRangeQuery object) { + return { + 'type': object.runtimeType.toString(), + 'data': object.toJson(), + }; + } +} diff --git a/packages/paperless_api/lib/src/converters/document_model_json_converter.dart b/packages/paperless_api/lib/src/converters/document_model_json_converter.dart new file mode 100644 index 0000000..24b7312 --- /dev/null +++ b/packages/paperless_api/lib/src/converters/document_model_json_converter.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/src/models/document_model.dart'; + +class DocumentModelJsonConverter + extends JsonConverter> { + @override + DocumentModel fromJson(Map json) { + return DocumentModel.fromJson(json); + } + + @override + Map toJson(DocumentModel object) { + return object.toJson(); + } +} diff --git a/packages/paperless_api/lib/src/converters/id_query_parameter_json_converter.dart b/packages/paperless_api/lib/src/converters/id_query_parameter_json_converter.dart deleted file mode 100644 index 0e1f5ad..0000000 --- a/packages/paperless_api/lib/src/converters/id_query_parameter_json_converter.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/models/models.dart'; - -class IdQueryParameterJsonConverter - extends JsonConverter> { - const IdQueryParameterJsonConverter(); - static const _idKey = "id"; - static const _assignmentStatusKey = 'assignmentStatus'; - @override - IdQueryParameter fromJson(Map json) { - return IdQueryParameter(json[_assignmentStatusKey], json[_idKey]); - } - - @override - Map toJson(IdQueryParameter object) { - return { - _idKey: object.id, - _assignmentStatusKey: object.assignmentStatus, - }; - } -} diff --git a/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart b/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart new file mode 100644 index 0000000..2b34c84 --- /dev/null +++ b/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/paperless_api.dart'; + +class SimilarDocumentModelJsonConverter + extends JsonConverter> { + @override + SimilarDocumentModel fromJson(Map json) { + return SimilarDocumentModel.fromJson(json); + } + + @override + Map toJson(SimilarDocumentModel object) { + return object.toJson(); + } +} diff --git a/packages/paperless_api/lib/src/converters/tags_query_json_converter.dart b/packages/paperless_api/lib/src/converters/tags_query_json_converter.dart new file mode 100644 index 0000000..643215a --- /dev/null +++ b/packages/paperless_api/lib/src/converters/tags_query_json_converter.dart @@ -0,0 +1,34 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/src/models/query_parameters/tags_query/any_assigned_tags_query.dart'; +import 'package:paperless_api/src/models/query_parameters/tags_query/ids_tags_query.dart'; +import 'package:paperless_api/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart'; + +import '../models/query_parameters/tags_query/tags_query.dart'; + +class TagsQueryJsonConverter + extends JsonConverter> { + const TagsQueryJsonConverter(); + @override + TagsQuery fromJson(Map json) { + final type = json['type'] as String; + final data = json['data'] as Map; + switch (type) { + case 'OnlyNotAssignedTagsQuery': + return const OnlyNotAssignedTagsQuery(); + case 'AnyAssignedTagsQuery': + return AnyAssignedTagsQuery.fromJson(data); + case 'IdsTagsQuery': + return IdsTagsQuery.fromJson(data); + default: + throw Exception('Error parsing TagsQuery: Unknown type $type'); + } + } + + @override + Map toJson(TagsQuery object) { + return { + 'type': object.runtimeType.toString(), + 'data': object.toJson(), + }; + } +} diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index 2b13359..7e7a7ed 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -1,10 +1,14 @@ +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; -import 'package:paperless_api/src/models/query_parameters/text_query.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:collection/collection.dart'; +import 'package:paperless_api/src/converters/tags_query_json_converter.dart'; -@JsonSerializable() +part 'document_filter.g.dart'; + +@TagsQueryJsonConverter() +@DateRangeQueryJsonConverter() +@JsonSerializable(explicitToJson: true) class DocumentFilter extends Equatable { static const DocumentFilter initial = DocumentFilter(); @@ -73,7 +77,7 @@ class DocumentFilter extends Equatable { key, entries.length == 1 ? entries.first.value - : entries.map((e) => e.value).toList(), + : entries.map((e) => e.value).join(","), ), ); return queryParams; @@ -150,4 +154,9 @@ class DocumentFilter extends Equatable { modified, query, ]; + + factory DocumentFilter.fromJson(Map json) => + _$DocumentFilterFromJson(json); + + Map toJson() => _$DocumentFilterToJson(this); } diff --git a/packages/paperless_api/lib/src/models/document_filter.g.dart b/packages/paperless_api/lib/src/models/document_filter.g.dart new file mode 100644 index 0000000..073c517 --- /dev/null +++ b/packages/paperless_api/lib/src/models/document_filter.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'document_filter.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DocumentFilter _$DocumentFilterFromJson(Map json) => + DocumentFilter( + documentType: json['documentType'] == null + ? const IdQueryParameter.unset() + : IdQueryParameter.fromJson( + json['documentType'] as Map), + correspondent: json['correspondent'] == null + ? const IdQueryParameter.unset() + : IdQueryParameter.fromJson( + json['correspondent'] as Map), + storagePath: json['storagePath'] == null + ? const IdQueryParameter.unset() + : IdQueryParameter.fromJson( + json['storagePath'] as Map), + asnQuery: json['asnQuery'] == null + ? const IdQueryParameter.unset() + : IdQueryParameter.fromJson(json['asnQuery'] as Map), + tags: json['tags'] == null + ? const IdsTagsQuery() + : const TagsQueryJsonConverter() + .fromJson(json['tags'] as Map), + sortField: $enumDecodeNullable(_$SortFieldEnumMap, json['sortField']) ?? + SortField.created, + sortOrder: $enumDecodeNullable(_$SortOrderEnumMap, json['sortOrder']) ?? + SortOrder.descending, + page: json['page'] as int? ?? 1, + pageSize: json['pageSize'] as int? ?? 25, + query: json['query'] == null + ? const TextQuery() + : TextQuery.fromJson(json['query'] as Map), + added: json['added'] == null + ? const UnsetDateRangeQuery() + : const DateRangeQueryJsonConverter() + .fromJson(json['added'] as Map), + created: json['created'] == null + ? const UnsetDateRangeQuery() + : const DateRangeQueryJsonConverter() + .fromJson(json['created'] as Map), + modified: json['modified'] == null + ? const UnsetDateRangeQuery() + : const DateRangeQueryJsonConverter() + .fromJson(json['modified'] as Map), + ); + +Map _$DocumentFilterToJson(DocumentFilter instance) => + { + 'pageSize': instance.pageSize, + 'page': instance.page, + 'documentType': instance.documentType.toJson(), + 'correspondent': instance.correspondent.toJson(), + 'storagePath': instance.storagePath.toJson(), + 'asnQuery': instance.asnQuery.toJson(), + 'tags': const TagsQueryJsonConverter().toJson(instance.tags), + 'sortField': _$SortFieldEnumMap[instance.sortField]!, + 'sortOrder': _$SortOrderEnumMap[instance.sortOrder]!, + 'created': const DateRangeQueryJsonConverter().toJson(instance.created), + 'added': const DateRangeQueryJsonConverter().toJson(instance.added), + 'modified': const DateRangeQueryJsonConverter().toJson(instance.modified), + 'query': instance.query.toJson(), + }; + +const _$SortFieldEnumMap = { + SortField.archiveSerialNumber: 'archiveSerialNumber', + SortField.correspondentName: 'correspondentName', + SortField.title: 'title', + SortField.documentType: 'documentType', + SortField.created: 'created', + SortField.added: 'added', + SortField.modified: 'modified', +}; + +const _$SortOrderEnumMap = { + SortOrder.ascending: 'ascending', + SortOrder.descending: 'descending', +}; diff --git a/packages/paperless_api/lib/src/models/filter_rule_model.dart b/packages/paperless_api/lib/src/models/filter_rule_model.dart index 389966d..d64f214 100644 --- a/packages/paperless_api/lib/src/models/filter_rule_model.dart +++ b/packages/paperless_api/lib/src/models/filter_rule_model.dart @@ -1,8 +1,17 @@ import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/constants.dart'; -import 'package:paperless_api/src/models/query_parameters/text_query.dart'; +import 'query_parameters/tags_query/any_assigned_tags_query.dart'; +import 'query_parameters/tags_query/exclude_tag_id_query.dart'; +import 'query_parameters/tags_query/ids_tags_query.dart'; +import 'query_parameters/tags_query/include_tag_id_query.dart'; +import 'query_parameters/tags_query/only_not_assigned_tags_query.dart'; + +part 'filter_rule_model.g.dart'; + +@JsonSerializable() class FilterRule with EquatableMixin { static const int titleRule = 0; static const int asnRule = 2; @@ -35,22 +44,12 @@ class FilterRule with EquatableMixin { static const String _lastNDateRangeQueryRegex = r"(?created|added|modified):\[-?(?\d+) (?day|week|month|year) to now\]"; + @JsonKey(name: 'rule_type') final int ruleType; final String? value; FilterRule(this.ruleType, this.value); - FilterRule.fromJson(Map json) - : ruleType = json['rule_type'], - value = json['value']; - - Map toJson() { - return { - 'rule_type': ruleType, - 'value': value, - }; - } - DocumentFilter applyToFilter(final DocumentFilter filter) { //TODO: Check in profiling mode if this is inefficient enough to cause stutters... switch (ruleType) { @@ -368,4 +367,9 @@ class FilterRule with EquatableMixin { @override List get props => [ruleType, value]; + + Map toJson() => _$FilterRuleToJson(this); + + factory FilterRule.fromJson(Map json) => + _$FilterRuleFromJson(json); } diff --git a/packages/paperless_api/lib/src/models/filter_rule_model.g.dart b/packages/paperless_api/lib/src/models/filter_rule_model.g.dart new file mode 100644 index 0000000..d9b38b7 --- /dev/null +++ b/packages/paperless_api/lib/src/models/filter_rule_model.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'filter_rule_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FilterRule _$FilterRuleFromJson(Map json) => FilterRule( + json['rule_type'] as int, + json['value'] as String?, + ); + +Map _$FilterRuleToJson(FilterRule instance) => + { + 'rule_type': instance.ruleType, + 'value': instance.value, + }; diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index 0e72519..5d39de9 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -8,8 +8,8 @@ export 'query_parameters/id_query_parameter.dart'; export 'query_parameters/query_type.dart'; export 'query_parameters/sort_field.dart'; export 'query_parameters/sort_order.dart'; -export 'query_parameters/tags_query.dart'; -export 'query_parameters/date_range_query.dart'; +export 'query_parameters/tags_query/tags_queries.dart'; +export 'query_parameters/date_range_queries/date_range_queries.dart'; export 'query_parameters/text_query.dart'; export 'bulk_edit_model.dart'; export 'document_filter.dart'; diff --git a/packages/paperless_api/lib/src/models/paged_search_result.dart b/packages/paperless_api/lib/src/models/paged_search_result.dart index eb6685a..4b510a6 100644 --- a/packages/paperless_api/lib/src/models/paged_search_result.dart +++ b/packages/paperless_api/lib/src/models/paged_search_result.dart @@ -4,15 +4,13 @@ import 'package:paperless_api/src/models/document_model.dart'; const pageRegex = r".*page=(\d+).*"; -//Todo: make this an interface and delegate serialization to implementations class PagedSearchResultJsonSerializer { final Map json; - final T Function(Map) fromJson; + JsonConverter> converter; - PagedSearchResultJsonSerializer(this.json, this.fromJson); + PagedSearchResultJsonSerializer(this.json, this.converter); } -@JsonSerializable() class PagedSearchResult extends Equatable { /// Total number of available items final int count; @@ -54,18 +52,34 @@ class PagedSearchResult extends Equatable { required this.results, }); - factory PagedSearchResult.fromJson( - PagedSearchResultJsonSerializer serializer) { + factory PagedSearchResult.fromJson(Map json, + JsonConverter> converter) { return PagedSearchResult( - count: serializer.json['count'], - next: serializer.json['next'], - previous: serializer.json['previous'], - results: List>.from(serializer.json['results']) - .map(serializer.fromJson) + count: json['count'], + next: json['next'], + previous: json['previous'], + results: List>.from(json['results']) + .map(converter.fromJson) .toList(), ); } + Map toJson( + JsonConverter> converter) { + return { + 'count': count, + 'next': next, + 'previous': previous, + 'results': results.map((e) => converter.toJson(e)).toList() + }; + } + + factory PagedSearchResult.fromJsonSingleParam( + PagedSearchResultJsonSerializer serializer, + ) { + return PagedSearchResult.fromJson(serializer.json, serializer.converter); + } + PagedSearchResult copyWith({ int? count, String? next, diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart new file mode 100644 index 0000000..b19abb2 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart @@ -0,0 +1,51 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/src/constants.dart'; + +import 'date_range_query.dart'; +import 'date_range_query_field.dart'; + +part 'absolute_date_range_query.g.dart'; + +@JsonSerializable() +class AbsoluteDateRangeQuery extends DateRangeQuery { + final DateTime? after; + final DateTime? before; + + const AbsoluteDateRangeQuery({this.after, this.before}); + + @override + List get props => [after, before]; + + @override + Map toQueryParameter(DateRangeQueryField field) { + final Map params = {}; + + // Add/subtract one day in the following because paperless uses gt/lt not gte/lte + if (after != null) { + params.putIfAbsent('${field.name}__date__gt', + () => apiDateFormat.format(after!.subtract(const Duration(days: 1)))); + } + + if (before != null) { + params.putIfAbsent('${field.name}__date__lt', + () => apiDateFormat.format(before!.add(const Duration(days: 1)))); + } + return params; + } + + AbsoluteDateRangeQuery copyWith({ + DateTime? before, + DateTime? after, + }) { + return AbsoluteDateRangeQuery( + before: before ?? this.before, + after: after ?? this.after, + ); + } + + factory AbsoluteDateRangeQuery.fromJson(json) => + _$AbsoluteDateRangeQueryFromJson(json); + + @override + Map toJson() => _$AbsoluteDateRangeQueryToJson(this); +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.g.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.g.dart new file mode 100644 index 0000000..9007991 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/absolute_date_range_query.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'absolute_date_range_query.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AbsoluteDateRangeQuery _$AbsoluteDateRangeQueryFromJson( + Map json) => + AbsoluteDateRangeQuery( + after: json['after'] == null + ? null + : DateTime.parse(json['after'] as String), + before: json['before'] == null + ? null + : DateTime.parse(json['before'] as String), + ); + +Map _$AbsoluteDateRangeQueryToJson( + AbsoluteDateRangeQuery instance) => + { + 'after': instance.after?.toIso8601String(), + 'before': instance.before?.toIso8601String(), + }; diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_queries.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_queries.dart new file mode 100644 index 0000000..81391b9 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_queries.dart @@ -0,0 +1,6 @@ +export 'date_range_query.dart'; +export 'unset_date_range_query.dart'; +export 'absolute_date_range_query.dart'; +export 'relative_date_range_query.dart'; +export 'date_range_unit.dart'; +export 'date_range_query_field.dart'; diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart new file mode 100644 index 0000000..5ce1fe8 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; + +import 'date_range_query_field.dart'; + +abstract class DateRangeQuery extends Equatable { + const DateRangeQuery(); + Map toQueryParameter(DateRangeQueryField field); + + Map toJson(); +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query_field.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query_field.dart new file mode 100644 index 0000000..6bd16f6 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_query_field.dart @@ -0,0 +1,5 @@ +enum DateRangeQueryField { + created, + added, + modified; +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_unit.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_unit.dart new file mode 100644 index 0000000..3c35f14 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/date_range_unit.dart @@ -0,0 +1,6 @@ +enum DateRangeUnit { + day, + week, + month, + year; +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart new file mode 100644 index 0000000..ae435b1 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.dart @@ -0,0 +1,43 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'date_range_query.dart'; +import 'date_range_query_field.dart'; +import 'date_range_unit.dart'; +part 'relative_date_range_query.g.dart'; + +@JsonSerializable() +class RelativeDateRangeQuery extends DateRangeQuery { + final int offset; + final DateRangeUnit unit; + + const RelativeDateRangeQuery([ + this.offset = 1, + this.unit = DateRangeUnit.day, + ]); + + @override + List get props => [offset, unit]; + + @override + Map toQueryParameter(DateRangeQueryField field) { + return { + 'query': '${field.name}:[-$offset ${unit.name} to now]', + }; + } + + RelativeDateRangeQuery copyWith({ + int? offset, + DateRangeUnit? unit, + }) { + return RelativeDateRangeQuery( + offset ?? this.offset, + unit ?? this.unit, + ); + } + + @override + Map toJson() => _$RelativeDateRangeQueryToJson(this); + + factory RelativeDateRangeQuery.fromJson(Map json) => + _$RelativeDateRangeQueryFromJson(json); +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.g.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.g.dart new file mode 100644 index 0000000..8828b25 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/relative_date_range_query.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'relative_date_range_query.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RelativeDateRangeQuery _$RelativeDateRangeQueryFromJson( + Map json) => + RelativeDateRangeQuery( + json['offset'] as int? ?? 1, + $enumDecodeNullable(_$DateRangeUnitEnumMap, json['unit']) ?? + DateRangeUnit.day, + ); + +Map _$RelativeDateRangeQueryToJson( + RelativeDateRangeQuery instance) => + { + 'offset': instance.offset, + 'unit': _$DateRangeUnitEnumMap[instance.unit]!, + }; + +const _$DateRangeUnitEnumMap = { + DateRangeUnit.day: 'day', + DateRangeUnit.week: 'week', + DateRangeUnit.month: 'month', + DateRangeUnit.year: 'year', +}; diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart new file mode 100644 index 0000000..055130f --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_queries/unset_date_range_query.dart @@ -0,0 +1,17 @@ +import 'package:paperless_api/src/models/query_parameters/date_range_queries/date_range_query_field.dart'; + +import 'date_range_query.dart'; + +class UnsetDateRangeQuery extends DateRangeQuery { + const UnsetDateRangeQuery(); + @override + List get props => []; + + @override + Map toQueryParameter(DateRangeQueryField field) => const {}; + + @override + Map toJson() { + return {}; + } +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart deleted file mode 100644 index 7d6d12c..0000000 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:paperless_api/src/constants.dart'; - -abstract class DateRangeQuery extends Equatable { - const DateRangeQuery(); - Map toQueryParameter(DateRangeQueryField field); -} - -class UnsetDateRangeQuery extends DateRangeQuery { - const UnsetDateRangeQuery(); - @override - List get props => []; - - @override - Map toQueryParameter(DateRangeQueryField field) => const {}; -} - -class AbsoluteDateRangeQuery extends DateRangeQuery { - final DateTime? after; - final DateTime? before; - - const AbsoluteDateRangeQuery({this.after, this.before}); - - @override - List get props => [after, before]; - - @override - Map toQueryParameter(DateRangeQueryField field) { - final Map params = {}; - - // Add/subtract one day in the following because paperless uses gt/lt not gte/lte - if (after != null) { - params.putIfAbsent('${field.name}__date__gt', - () => apiDateFormat.format(after!.subtract(const Duration(days: 1)))); - } - - if (before != null) { - params.putIfAbsent('${field.name}__date__lt', - () => apiDateFormat.format(before!.add(const Duration(days: 1)))); - } - return params; - } - - AbsoluteDateRangeQuery copyWith({ - DateTime? before, - DateTime? after, - }) { - return AbsoluteDateRangeQuery( - before: before ?? this.before, - after: after ?? this.after, - ); - } -} - -class RelativeDateRangeQuery extends DateRangeQuery { - final int offset; - final DateRangeUnit unit; - - const RelativeDateRangeQuery([ - this.offset = 1, - this.unit = DateRangeUnit.day, - ]); - - @override - List get props => [offset, unit]; - - @override - Map toQueryParameter(DateRangeQueryField field) { - return { - 'query': '${field.name}:[-$offset ${unit.name} to now]', - }; - } - - RelativeDateRangeQuery copyWith({ - int? offset, - DateRangeUnit? unit, - }) { - return RelativeDateRangeQuery( - offset ?? this.offset, - unit ?? this.unit, - ); - } -} - -enum DateRangeUnit { - day, - week, - month, - year; -} - -enum DateRangeQueryField { - created, - added, - modified; -} diff --git a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart index 9a6f1a3..1890e2a 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart @@ -1,49 +1,43 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/converters/id_query_parameter_json_converter.dart'; +part 'id_query_parameter.g.dart'; -@IdQueryParameterJsonConverter() @JsonSerializable() class IdQueryParameter extends Equatable { - final int? _assignmentStatus; - final int? _id; + final int? assignmentStatus; + final int? id; @Deprecated("Use named constructors, this is only meant for code generation") - const IdQueryParameter(this._assignmentStatus, this._id); + const IdQueryParameter(this.assignmentStatus, this.id); const IdQueryParameter.notAssigned() - : _assignmentStatus = 1, - _id = null; + : assignmentStatus = 1, + id = null; const IdQueryParameter.anyAssigned() - : _assignmentStatus = 0, - _id = null; + : assignmentStatus = 0, + id = null; const IdQueryParameter.fromId(int? id) - : _assignmentStatus = null, - _id = id; + : assignmentStatus = null, + id = id; const IdQueryParameter.unset() : this.fromId(null); - bool get isUnset => _id == null && _assignmentStatus == null; + bool get isUnset => id == null && assignmentStatus == null; - bool get isSet => _id != null && _assignmentStatus == null; + bool get isSet => id != null && assignmentStatus == null; - bool get onlyNotAssigned => _assignmentStatus == 1; + bool get onlyNotAssigned => assignmentStatus == 1; - bool get onlyAssigned => _assignmentStatus == 0; - - int? get id => _id; - - @visibleForTesting - int? get assignmentStatus => _assignmentStatus; + bool get onlyAssigned => assignmentStatus == 0; Map toQueryParameter(String field) { final Map params = {}; if (onlyNotAssigned || onlyAssigned) { params.putIfAbsent( - '${field}__isnull', () => _assignmentStatus!.toString()); + '${field}__isnull', () => assignmentStatus!.toString()); } if (isSet) { params.putIfAbsent("${field}__id", () => id!.toString()); @@ -52,5 +46,10 @@ class IdQueryParameter extends Equatable { } @override - List get props => [_assignmentStatus, _id]; + List get props => [assignmentStatus, id]; + + Map toJson() => _$IdQueryParameterToJson(this); + + factory IdQueryParameter.fromJson(Map json) => + _$IdQueryParameterFromJson(json); } diff --git a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.g.dart b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.g.dart new file mode 100644 index 0000000..d4d5a11 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'id_query_parameter.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +IdQueryParameter _$IdQueryParameterFromJson(Map json) => + IdQueryParameter( + json['assignmentStatus'] as int?, + json['id'] as int?, + ); + +Map _$IdQueryParameterToJson(IdQueryParameter instance) => + { + 'assignmentStatus': instance.assignmentStatus, + 'id': instance.id, + }; diff --git a/packages/paperless_api/lib/src/models/query_parameters/private/tags_query_type.dart b/packages/paperless_api/lib/src/models/query_parameters/private/tags_query_type.dart new file mode 100644 index 0000000..c62a6a2 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/private/tags_query_type.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() +enum TagsQueryType { + notAssigned, + anyAssigned, + ids, + id, + include, + exclude; +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart b/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart index ca6c0c8..cd337b5 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/sort_field.dart @@ -1,3 +1,6 @@ +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() enum SortField { archiveSerialNumber("archive_serial_number"), correspondentName("correspondent__name"), diff --git a/packages/paperless_api/lib/src/models/query_parameters/sort_order.dart b/packages/paperless_api/lib/src/models/query_parameters/sort_order.dart index 3962b30..d11d0d2 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/sort_order.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/sort_order.dart @@ -1,3 +1,6 @@ +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() enum SortOrder { ascending(""), descending("-"); diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart new file mode 100644 index 0000000..adf5a25 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'tags_query.dart'; +part 'any_assigned_tags_query.g.dart'; + +@JsonSerializable(explicitToJson: true) +class AnyAssignedTagsQuery extends TagsQuery { + final Iterable tagIds; + + const AnyAssignedTagsQuery({ + this.tagIds = const [], + }); + + @override + Map toQueryParameter() { + if (tagIds.isEmpty) { + return {'is_tagged': '1'}; + } + return {'tags__id__in': tagIds.join(',')}; + } + + @override + List get props => [tagIds]; + + @override + Map toJson() => _$AnyAssignedTagsQueryToJson(this); + + factory AnyAssignedTagsQuery.fromJson(Map json) => + _$AnyAssignedTagsQueryFromJson(json); +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.g.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.g.dart new file mode 100644 index 0000000..57a227d --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/any_assigned_tags_query.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'any_assigned_tags_query.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AnyAssignedTagsQuery _$AnyAssignedTagsQueryFromJson( + Map json) => + AnyAssignedTagsQuery( + tagIds: + (json['tagIds'] as List?)?.map((e) => e as int) ?? const [], + ); + +Map _$AnyAssignedTagsQueryToJson( + AnyAssignedTagsQuery instance) => + { + 'tagIds': instance.tagIds.toList(), + }; diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/exclude_tag_id_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/exclude_tag_id_query.dart new file mode 100644 index 0000000..69fdbdb --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/exclude_tag_id_query.dart @@ -0,0 +1,15 @@ +import 'package:paperless_api/src/models/query_parameters/tags_query/tag_id_query.dart'; + +import 'include_tag_id_query.dart'; + +class ExcludeTagIdQuery extends TagIdQuery { + const ExcludeTagIdQuery(super.id); + + @override + String get methodName => 'exclude'; + + @override + TagIdQuery toggle() { + return IncludeTagIdQuery(id); + } +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart similarity index 59% rename from packages/paperless_api/lib/src/models/query_parameters/tags_query.dart rename to packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart index f025d23..0734932 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.dart @@ -1,40 +1,13 @@ -import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; -abstract class TagsQuery extends Equatable { - const TagsQuery(); - Map toQueryParameter(); -} +import 'exclude_tag_id_query.dart'; +import 'include_tag_id_query.dart'; +import 'tag_id_query.dart'; +import 'tags_query.dart'; -class OnlyNotAssignedTagsQuery extends TagsQuery { - const OnlyNotAssignedTagsQuery(); - @override - Map toQueryParameter() { - return {'is_tagged': '0'}; - } - - @override - List get props => []; -} - -class AnyAssignedTagsQuery extends TagsQuery { - final Iterable tagIds; - - const AnyAssignedTagsQuery({ - this.tagIds = const [], - }); - - @override - Map toQueryParameter() { - if (tagIds.isEmpty) { - return {'is_tagged': '1'}; - } - return {'tags__id__in': tagIds.join(',')}; - } - - @override - List get props => [tagIds]; -} +part 'ids_tags_query.g.dart'; +@JsonSerializable(explicitToJson: true) class IdsTagsQuery extends TagsQuery { final Iterable _idQueries; @@ -102,41 +75,10 @@ class IdsTagsQuery extends TagsQuery { @override List get props => [_idQueries]; -} - -abstract class TagIdQuery extends Equatable { - final int id; - - const TagIdQuery(this.id); - - String get methodName; - - @override - List get props => [id, methodName]; - - TagIdQuery toggle(); -} - -class IncludeTagIdQuery extends TagIdQuery { - const IncludeTagIdQuery(super.id); - - @override - String get methodName => 'include'; - - @override - TagIdQuery toggle() { - return ExcludeTagIdQuery(id); - } -} - -class ExcludeTagIdQuery extends TagIdQuery { - const ExcludeTagIdQuery(super.id); - - @override - String get methodName => 'exclude'; - - @override - TagIdQuery toggle() { - return IncludeTagIdQuery(id); - } + + @override + Map toJson() => _$IdsTagsQueryToJson(this); + + factory IdsTagsQuery.fromJson(Map json) => + _$IdsTagsQueryFromJson(json); } diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.g.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.g.dart new file mode 100644 index 0000000..9b5c5cf --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/ids_tags_query.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ids_tags_query.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +IdsTagsQuery _$IdsTagsQueryFromJson(Map json) => + IdsTagsQuery(); + +Map _$IdsTagsQueryToJson(IdsTagsQuery instance) => + {}; diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/include_tag_id_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/include_tag_id_query.dart new file mode 100644 index 0000000..f0b6686 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/include_tag_id_query.dart @@ -0,0 +1,15 @@ +import 'package:paperless_api/src/models/query_parameters/tags_query/tag_id_query.dart'; + +import 'exclude_tag_id_query.dart'; + +class IncludeTagIdQuery extends TagIdQuery { + const IncludeTagIdQuery(super.id); + + @override + String get methodName => 'include'; + + @override + TagIdQuery toggle() { + return ExcludeTagIdQuery(id); + } +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart new file mode 100644 index 0000000..0c0d937 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart @@ -0,0 +1,17 @@ +import 'tags_query.dart'; + +class OnlyNotAssignedTagsQuery extends TagsQuery { + const OnlyNotAssignedTagsQuery(); + @override + Map toQueryParameter() { + return {'is_tagged': '0'}; + } + + @override + List get props => []; + + @override + Map toJson() { + return {}; + } +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/tag_id_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tag_id_query.dart new file mode 100644 index 0000000..e3e6708 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tag_id_query.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +abstract class TagIdQuery extends Equatable { + final int id; + + const TagIdQuery(this.id); + + String get methodName; + + @override + List get props => [id, methodName]; + + TagIdQuery toggle(); +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_queries.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_queries.dart new file mode 100644 index 0000000..9529320 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_queries.dart @@ -0,0 +1,7 @@ +export 'any_assigned_tags_query.dart'; +export 'ids_tags_query.dart'; +export 'tags_query.dart'; +export 'exclude_tag_id_query.dart'; +export 'include_tag_id_query.dart'; +export 'only_not_assigned_tags_query.dart'; +export 'tag_id_query.dart'; diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart new file mode 100644 index 0000000..b9de435 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query/tags_query.dart @@ -0,0 +1,7 @@ +import 'package:equatable/equatable.dart'; + +abstract class TagsQuery extends Equatable { + const TagsQuery(); + Map toQueryParameter(); + Map toJson(); +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart index aac8444..5b8f676 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/text_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/text_query.dart @@ -1,5 +1,10 @@ +import 'package:json_annotation/json_annotation.dart'; + import 'query_type.dart'; +part 'text_query.g.dart'; + +@JsonSerializable() class TextQuery { final QueryType queryType; final String? queryText; @@ -51,4 +56,9 @@ class TextQuery { } return null; } + + Map toJson() => _$TextQueryToJson(this); + + factory TextQuery.fromJson(Map json) => + _$TextQueryFromJson(json); } diff --git a/packages/paperless_api/lib/src/models/query_parameters/text_query.g.dart b/packages/paperless_api/lib/src/models/query_parameters/text_query.g.dart new file mode 100644 index 0000000..36cba1e --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/text_query.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'text_query.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TextQuery _$TextQueryFromJson(Map json) => TextQuery( + queryType: $enumDecodeNullable(_$QueryTypeEnumMap, json['queryType']) ?? + QueryType.titleAndContent, + queryText: json['queryText'] as String?, + ); + +Map _$TextQueryToJson(TextQuery instance) => { + 'queryType': _$QueryTypeEnumMap[instance.queryType]!, + 'queryText': instance.queryText, + }; + +const _$QueryTypeEnumMap = { + QueryType.title: 'title', + QueryType.titleAndContent: 'titleAndContent', + QueryType.extended: 'extended', + QueryType.asn: 'asn', +}; diff --git a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart index 5533ae0..171f67d 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:dio/dio.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart'; import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart'; @@ -24,10 +22,10 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { }, ); } on DioError catch (error) { - if (error.error is PaperlessServerException) { + if (error.error is PaperlessServerException || + error.error is Map) { throw error.error; } else { - log(error.message); throw PaperlessServerException( ErrorCode.authenticationFailed, details: error.message, diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index 6043b4e..e8cc60f 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -1,10 +1,11 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/constants.dart'; +import 'package:paperless_api/src/converters/document_model_json_converter.dart'; +import 'package:paperless_api/src/converters/similar_document_model_json_converter.dart'; class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { final Dio client; @@ -28,9 +29,8 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { 'document', MultipartFile.fromBytes(documentBytes, filename: filename), ), - ); - - formData.fields.add(MapEntry('title', title)); + ) + ..fields.add(MapEntry('title', title)); if (createdAt != null) { formData.fields.add(MapEntry('created', apiDateFormat.format(createdAt))); } @@ -85,10 +85,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { ); if (response.statusCode == 200) { return compute( - PagedSearchResult.fromJson, + PagedSearchResult.fromJsonSingleParam, PagedSearchResultJsonSerializer( response.data, - DocumentModel.fromJson, + DocumentModelJsonConverter(), ), ); } else { @@ -254,10 +254,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { await client.get("/api/documents/?more_like=$docId&pageSize=10"); if (response.statusCode == 200) { return (await compute( - PagedSearchResult.fromJson, + PagedSearchResult.fromJsonSingleParam, PagedSearchResultJsonSerializer( response.data, - SimilarDocumentModel.fromJson, + SimilarDocumentModelJsonConverter(), ), )) .results; diff --git a/packages/paperless_api/pubspec.yaml b/packages/paperless_api/pubspec.yaml index 0084c4c..91162d0 100644 --- a/packages/paperless_api/pubspec.yaml +++ b/packages/paperless_api/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: json_annotation: ^4.7.0 intl: ^0.17.0 dio: ^4.0.6 + collection: ^1.17.0 dev_dependencies: flutter_test: diff --git a/packages/paperless_api/test/saved_view_test.dart b/packages/paperless_api/test/saved_view_test.dart index 61e50c8..8bc10cd 100644 --- a/packages/paperless_api/test/saved_view_test.dart +++ b/packages/paperless_api/test/saved_view_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_api/src/models/query_parameters/tags_query/include_tag_id_query.dart'; import 'package:paperless_api/src/models/query_parameters/text_query.dart'; void main() { diff --git a/pubspec.lock b/pubspec.lock index 1b5b772..3550036 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8c7478991c7bbde2c1e18034ac697723176a5d3e7e0ca06c7f9aed69b6f388d7" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" url: "https://pub.dev" source: hosted - version: "51.0.0" + version: "52.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "120fe7ce25377ba616bb210e7584983b163861f45d6ec446744d507e3943881b" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.4.0" archive: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "02ce3596b459c666530f045ad6f96209474e8fee6e4855940a3cee65fb872ec5" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.0" collection: dependency: "direct main" description: @@ -330,7 +330,7 @@ packages: source: hosted version: "3.2.2" device_info_plus: - dependency: transitive + dependency: "direct main" description: name: device_info_plus sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6 @@ -406,7 +406,7 @@ packages: description: path: "." ref: master - resolved-ref: "2d417dd77e075cb12e82a390e50cc4554e877ec4" + resolved-ref: "8c80e3a6e231985763ff501ad7ae12d76995a2e8" url: "https://github.com/sawankumarbundelkhandi/edge_detection" source: git version: "1.1.1" @@ -470,10 +470,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: f9245fc33aeba9e0b938d7f3785f10b7a7230e05b8fc40f5a6a8342d7899e391 + sha256: ecf52f978e72763ede54a93271318bbbca65a2be2d9ff658ec8ca4ea3a23d7ef url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "5.2.4" fixnum: dependency: transitive description: @@ -756,10 +756,10 @@ packages: dependency: "direct main" description: name: image - sha256: f6ffe2895e3c86c6ad5a27e6302cf807403463e397cb2f0c580f619ac2fa588b + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.3.0" infinite_scroll_pagination: dependency: "direct main" description: @@ -809,10 +809,10 @@ packages: dependency: "direct main" description: name: introduction_screen - sha256: "307869da307a6ba291008f8d06030816e94fe0759f6e34d671f9c39c4d512937" + sha256: "26d06cff940b9f3f1ec6591a6beea4da31183574b279c373e142ca76882ce9ea" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" io: dependency: transitive description: @@ -857,34 +857,34 @@ packages: dependency: "direct main" description: name: local_auth - sha256: "792b06b9e7deb52f1f55b5de678a319261c395e61d804e0f3f97c732cf002aef" + sha256: "8cea55dca20d1e0efa5480df2d47ae30851e7a24cb8e7d225be7e67ae8485aa4" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" local_auth_android: dependency: transitive description: name: local_auth_android - sha256: "95cfa61a43e0b4307d7b0abb94cad71fe47601292cafb54c2205b97b9f958fb8" + sha256: ba48fe0e1cae140a0813ce68c2540250d7f573a8ae4d4b6c681b2d2583584953 url: "https://pub.dev" source: hosted - version: "1.0.16" + version: "1.0.17" local_auth_ios: dependency: transitive description: name: local_auth_ios - sha256: "1600673a460e60ff068af91dc80b507b2ac811645db439f757521dfced0d0ab2" + sha256: aa32478d7513066564139af57e11e2cad1bbd535c1efd224a88a8764c5665e3b url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "1.0.12" local_auth_platform_interface: dependency: transitive description: name: local_auth_platform_interface - sha256: b069647f81ed12c833d3f1f7c2880e20b86ca04429e786df76731ac2d4220c47 + sha256: fbb6973f2fd088e2677f39a5ab550aa1cfbc00997859d5e865569872499d6d61 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" local_auth_windows: dependency: transitive description: @@ -1152,18 +1152,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "5749ebeb7ec0c3865ea17e3eb337174b87747be816dab582c551e1aff6f6bbf3" + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "10.2.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: a512e0fa8abcb0659d938ec2df93a70eb1df1fdea5fdc6d79a866bfd858a28fc + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" url: "https://pub.dev" source: hosted - version: "9.0.2+1" + version: "10.2.0" permission_handler_apple: dependency: transitive description: @@ -1762,5 +1762,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.18.5 <4.0.0" + dart: ">=3.0.0-35.0.dev <4.0.0" flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index 12ca612..5d26fbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.4.0+11 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: '>=3.0.0-35.0.dev <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -33,7 +33,7 @@ dependencies: flutter_localizations: sdk: flutter encrypted_shared_preferences: ^3.0.0 - permission_handler: ^9.2.0 + permission_handler: ^10.2.0 pdf: ^3.8.1 pdfx: ^2.3.0 edge_detection: @@ -46,7 +46,7 @@ dependencies: intl: ^0.17.0 flutter_svg: ^1.0.3 url_launcher: ^6.1.2 - file_picker: ^3.0.4 + file_picker: ^5.2.4 web_socket_channel: ^2.2.0 http: ^0.13.4 http_interceptor: ^2.0.0-beta.5 @@ -83,6 +83,7 @@ dependencies: json_annotation: ^4.7.0 pretty_dio_logger: ^1.2.0-beta-1 collection: ^1.17.0 + device_info_plus: ^4.1.3 dev_dependencies: integration_test: diff --git a/test/src/bloc/document_cubit_test.dart b/test/src/bloc/document_cubit_test.dart index 44465e5..d639fcd 100644 --- a/test/src/bloc/document_cubit_test.dart +++ b/test/src/bloc/document_cubit_test.dart @@ -35,7 +35,7 @@ void main() async { group("Test DocumentsCubit reloadDocuments", () { test("Assert correct initial state", () { - expect(DocumentsCubit(documentRepository).state, DocumentsState.initial); + expect(DocumentsCubit(documentRepository).state, const DocumentsState()); }); blocTest( @@ -49,11 +49,11 @@ void main() async { ), ), build: () => DocumentsCubit(documentRepository), - seed: () => DocumentsState.initial, + seed: () => const DocumentsState(), act: (bloc) => bloc.load(), expect: () => [ DocumentsState( - isLoaded: true, + hasLoaded: true, value: [ PagedSearchResult( count: 10, @@ -78,11 +78,11 @@ void main() async { ), ), build: () => DocumentsCubit(documentRepository), - seed: () => DocumentsState.initial, + seed: () => const DocumentsState(), act: (bloc) => bloc.load(), expect: () => [ DocumentsState( - isLoaded: true, + hasLoaded: true, value: [ PagedSearchResult( count: 10,