From f390aa6c6a27536b7051f06628c26ad5df2eabdb Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 31 Dec 2022 01:35:26 +0100 Subject: [PATCH] Added server address validation, success message, localization --- .../authentication_aware_dio_manager.dart | 2 + .../extended_date_range_dialog.dart | 9 +- ...orm_builder_relative_date_range_field.dart | 12 +- .../documents/view/pages/documents_page.dart | 6 +- .../widgets/search/document_filter_panel.dart | 117 ++++++++++-------- .../view/widget/verify_identity_page.dart | 76 ++++++++++++ .../widgets/server_address_form_field.dart | 49 ++++++-- lib/l10n/intl_cs.arb | 46 +++++-- lib/l10n/intl_de.arb | 32 ++++- lib/l10n/intl_en.arb | 30 ++++- lib/main.dart | 53 +------- lib/util.dart | 20 ++- .../authentication_api_impl.dart | 9 +- .../labels_api/paperless_labels_api_impl.dart | 4 +- pubspec.yaml | 1 - 15 files changed, 315 insertions(+), 151 deletions(-) create mode 100644 lib/features/home/view/widget/verify_identity_page.dart diff --git a/lib/core/security/authentication_aware_dio_manager.dart b/lib/core/security/authentication_aware_dio_manager.dart index 623177c..60db727 100644 --- a/lib/core/security/authentication_aware_dio_manager.dart +++ b/lib/core/security/authentication_aware_dio_manager.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; +import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; import 'package:paperless_mobile/extensions/security_context_extension.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; @@ -23,6 +24,7 @@ class AuthenticationAwareDioManager { (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) => client..badCertificateCallback = (cert, host, port) => true; dio.interceptors.addAll(interceptors); + dio.interceptors.add(RetryOnConnectionChangeInterceptor(dio: dio)); return dio; } diff --git a/lib/core/widgets/form_builder_fields/extended_date_range_form_field/extended_date_range_dialog.dart b/lib/core/widgets/form_builder_fields/extended_date_range_form_field/extended_date_range_dialog.dart index 967c965..b84be38 100644 --- a/lib/core/widgets/form_builder_fields/extended_date_range_form_field/extended_date_range_dialog.dart +++ b/lib/core/widgets/form_builder_fields/extended_date_range_form_field/extended_date_range_dialog.dart @@ -46,7 +46,8 @@ class _ExtendedDateRangeDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text("Select date range"), + insetPadding: const EdgeInsets.all(24.0), + title: Text(S.of(context).extendedDateRangeDialogTitle), content: FormBuilder( key: _formKey, child: Column( @@ -55,7 +56,7 @@ class _ExtendedDateRangeDialogState extends State { children: [ _buildDateRangeQueryTypeSelection(), Text( - "Hint: You can either specify absolute values by selecting concrete dates, or you can specify a time range relative to the current date.", + S.of(context).extendedDateRangeDialogHintText, style: Theme.of(context).textTheme.bodySmall, ).paddedOnly(top: 8, bottom: 16), Builder( @@ -109,12 +110,12 @@ class _ExtendedDateRangeDialogState extends State { ButtonSegment( value: DateRangeType.absolute, enabled: true, - label: Text("Absolute"), + label: Text(S.of(context).extendedDateRangeDialogAbsoluteLabel), ), ButtonSegment( value: DateRangeType.relative, enabled: true, - label: Text("Relative"), + label: Text(S.of(context).extendedDateRangeDialogRelativeLabel), ), ], selected: {_selectedDateRangeType}, diff --git a/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart b/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart index 95986ec..41d992a 100644 --- a/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart +++ b/lib/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart @@ -47,12 +47,14 @@ class _FormBuilderRelativeDateRangePickerState Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("Last"), + Text(S.of(context).extendedDateRangeDialogRelativeLastLabel), SizedBox( - width: 70, + width: 80, child: TextFormField( decoration: InputDecoration( - labelText: "Offset", + labelText: S + .of(context) + .extendedDateRangeDialogRelativeAmountLabel, ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -91,7 +93,9 @@ class _FormBuilderRelativeDateRangePickerState onChanged: (value) => field.didChange(field.value!.copyWith(unit: value)), decoration: InputDecoration( - labelText: "Amount", + labelText: S + .of(context) + .extendedDateRangeDialogRelativeTimeUnitLabel, ), ), ), diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 47e86cf..8ea2df1 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -83,8 +83,8 @@ class _DocumentsPageState extends State { -4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd isLabelVisible: appliedFiltersCount > 0, count: state.filter.appliedFiltersCount, - backgroundColor: Theme.of(context).colorScheme.errorContainer, - textColor: Theme.of(context).colorScheme.onErrorContainer, + backgroundColor: Colors.red, + textColor: Colors.white, child: FloatingActionButton( child: const Icon(Icons.filter_alt_outlined), onPressed: _openDocumentFilter, @@ -115,7 +115,7 @@ class _DocumentsPageState extends State { expand: false, snap: true, initialChildSize: .9, - maxChildSize: .9, + snapSizes: const [.9, 1], builder: (context, controller) => LabelsBlocProvider( child: DocumentFilterPanel( initialFilter: context.read().state.filter, diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 80d8af4..179f132 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -70,65 +70,74 @@ class _DocumentFilterPanelState extends State { resizeToAvoidBottomInset: true, body: FormBuilder( key: _formKey, - child: ListView( - controller: widget.scrollController, - children: [ - Align( - alignment: Alignment.center, - child: Container( - width: 32, - height: 4, - margin: const EdgeInsets.only(top: 16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurfaceVariant, - borderRadius: BorderRadius.circular(16), - ), - ), - ), - Text( - S.of(context).documentFilterTitle, - style: Theme.of(context).textTheme.headlineSmall, - ).paddedOnly( - top: 16.0, - left: 16.0, - bottom: 24, - ), - Align( - alignment: Alignment.centerLeft, - child: Text( - S.of(context).documentFilterSearchLabel, - style: Theme.of(context).textTheme.bodySmall, - ), - ).paddedOnly(left: 8.0), - _buildQueryFormField().padded(), - Align( - alignment: Alignment.centerLeft, - child: Text( - S.of(context).documentFilterAdvancedLabel, - style: Theme.of(context).textTheme.bodySmall, - ), - ).padded(), - FormBuilderExtendedDateRangePicker( - name: DocumentModel.createdKey, - initialValue: widget.initialFilter.created, - labelText: S.of(context).documentCreatedPropertyLabel, - ).padded(), - FormBuilderExtendedDateRangePicker( - name: DocumentModel.addedKey, - initialValue: widget.initialFilter.added, - labelText: S.of(context).documentAddedPropertyLabel, - ).padded(), - _buildCorrespondentFormField().padded(), - _buildDocumentTypeFormField().padded(), - _buildStoragePathFormField().padded(), - _buildTagsFormField().padded(), - ], - ).paddedOnly(bottom: 16), + child: _buildFormList(context), ), ), ); } + ListView _buildFormList(BuildContext context) { + return ListView( + controller: widget.scrollController, + children: [ + Align( + alignment: Alignment.center, + child: _buildDragHandle(context), + ), + Text( + S.of(context).documentFilterTitle, + style: Theme.of(context).textTheme.headlineSmall, + ).paddedOnly( + top: 16.0, + left: 16.0, + bottom: 24, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + S.of(context).documentFilterSearchLabel, + style: Theme.of(context).textTheme.bodySmall, + ), + ).paddedOnly(left: 8.0), + _buildQueryFormField().padded(), + Align( + alignment: Alignment.centerLeft, + child: Text( + S.of(context).documentFilterAdvancedLabel, + style: Theme.of(context).textTheme.bodySmall, + ), + ).padded(), + FormBuilderExtendedDateRangePicker( + name: DocumentModel.createdKey, + initialValue: widget.initialFilter.created, + labelText: S.of(context).documentCreatedPropertyLabel, + ).padded(), + FormBuilderExtendedDateRangePicker( + name: DocumentModel.addedKey, + initialValue: widget.initialFilter.added, + labelText: S.of(context).documentAddedPropertyLabel, + ).padded(), + _buildCorrespondentFormField().padded(), + _buildDocumentTypeFormField().padded(), + _buildStoragePathFormField().padded(), + _buildTagsFormField().padded(), + ], + ); + } + + Container _buildDragHandle(BuildContext context) { + return Container( + // According to m3 spec + width: 32, + height: 4, + margin: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4), + borderRadius: BorderRadius.circular(16), + ), + ); + } + BlocBuilder, LabelState> _buildTagsFormField() { return BlocBuilder, LabelState>( builder: (context, state) { diff --git a/lib/features/home/view/widget/verify_identity_page.dart b/lib/features/home/view/widget/verify_identity_page.dart new file mode 100644 index 0000000..cc22c3a --- /dev/null +++ b/lib/features/home/view/widget/verify_identity_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:provider/provider.dart'; + +class VerifyIdentityPage extends StatelessWidget { + const VerifyIdentityPage({super.key}); + + @override + Widget build(BuildContext context) { + return Material( + child: Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.background, + title: Text(S.of(context).verifyIdentityPageTitle), + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).verifyIdentityPageDescriptionText) + .paddedSymmetrically(horizontal: 16), + const Icon( + Icons.fingerprint, + size: 96, + ), + Wrap( + alignment: WrapAlignment.spaceBetween, + runAlignment: WrapAlignment.spaceBetween, + runSpacing: 8, + spacing: 8, + children: [ + TextButton( + onPressed: () => _logout(context), + child: Text( + S.of(context).verifyIdentityPageLogoutButtonLabel, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + ElevatedButton( + onPressed: () => context + .read() + .restoreSessionState(context + .read() + .state + .isLocalAuthenticationEnabled), + child: Text(S + .of(context) + .verifyIdentityPageVerifyIdentityButtonLabel), + ), + ], + ).padded(16), + ], + ), + ), + ); + } + + void _logout(BuildContext context) { + context.read().logout(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); + context.read>().clear(); + context.read().clear(); + HydratedBloc.storage.clear(); + } +} diff --git a/lib/features/login/view/widgets/server_address_form_field.dart b/lib/features/login/view/widgets/server_address_form_field.dart index db2bba7..1f16812 100644 --- a/lib/features/login/view/widgets/server_address_form_field.dart +++ b/lib/features/login/view/widgets/server_address_form_field.dart @@ -17,26 +17,49 @@ class ServerAddressFormField extends StatefulWidget { } class _ServerAddressFormFieldState extends State { + static const _ipv4Regex = r"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}"; + static const _ipv6Regex = + r"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"; + static final _urlRegex = RegExp( + r"^(https?:\/\/)(([\da-z\.-]+)\.([a-z\.]{2,6})|(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))(:\d{1,5})?([\/\w \.-]*)*\/?$"); + final TextEditingController _textEditingController = TextEditingController(); ReachabilityStatus _reachabilityStatus = ReachabilityStatus.undefined; @override Widget build(BuildContext context) { return FormBuilderTextField( key: const ValueKey('login-server-address'), + controller: _textEditingController, name: ServerAddressFormField.fkServerAddress, - validator: FormBuilderValidators.required( - errorText: S.of(context).loginPageServerUrlValidatorMessageText, + validator: FormBuilderValidators.compose( + [ + FormBuilderValidators.required( + errorText: + S.of(context).loginPageServerUrlValidatorMessageRequiredText, + ), + FormBuilderValidators.match( + _urlRegex.pattern, + errorText: S + .of(context) + .loginPageServerUrlValidatorMessageInvalidAddressText, + ), + ], ), - inputFormatters: [ - FilteringTextInputFormatter.deny(r".*/$"), - FilteringTextInputFormatter.deny(r"\s"), - ], decoration: InputDecoration( suffixIcon: _buildIsReachableIcon(), hintText: "http://192.168.1.50:8000", labelText: S.of(context).loginPageServerUrlFieldLabel, ), onChanged: _updateIsAddressReachableStatus, + onSubmitted: (value) { + if (value == null) return; + // Remove trailing slash if it is a valid address. + final address = value.trim(); + _textEditingController.text = address; + if (_urlRegex.hasMatch(address) && address.endsWith("/")) { + _textEditingController.text = address.replaceAll(RegExp(r'\/$'), ''); + } + }, ); } @@ -60,7 +83,7 @@ class _ServerAddressFormFieldState extends State { } void _updateIsAddressReachableStatus(String? address) async { - if (address == null || address.isEmpty) { + if (address == null || !_urlRegex.hasMatch(address)) { setState(() { _reachabilityStatus = ReachabilityStatus.undefined; }); @@ -70,12 +93,12 @@ class _ServerAddressFormFieldState extends State { setState(() => _reachabilityStatus = ReachabilityStatus.testing); final isReachable = await context .read() - .isServerReachable(address); - if (isReachable) { - setState(() => _reachabilityStatus = ReachabilityStatus.reachable); - } else { - setState(() => _reachabilityStatus = ReachabilityStatus.notReachable); - } + .isServerReachable(address.trim()); + setState( + () => _reachabilityStatus = isReachable + ? ReachabilityStatus.reachable + : ReachabilityStatus.notReachable, + ); } } diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index a49856a..8afb7c9 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -260,11 +260,25 @@ "@errorMessageUnsupportedFileFormat": {}, "errorReportLabel": "NAHLÁSIT", "@errorReportLabel": {}, - "extendedDateRangePickerAfterLabel": "", + "extendedDateRangeDialogAbsoluteLabel": "Absolute", + "@extendedDateRangeDialogAbsoluteLabel": {}, + "extendedDateRangeDialogHintText": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.", + "@extendedDateRangeDialogHintText": {}, + "extendedDateRangeDialogRelativeAmountLabel": "Amount", + "@extendedDateRangeDialogRelativeAmountLabel": {}, + "extendedDateRangeDialogRelativeLabel": "Relative", + "@extendedDateRangeDialogRelativeLabel": {}, + "extendedDateRangeDialogRelativeLastLabel": "Last", + "@extendedDateRangeDialogRelativeLastLabel": {}, + "extendedDateRangeDialogRelativeTimeUnitLabel": "Time unit", + "@extendedDateRangeDialogRelativeTimeUnitLabel": {}, + "extendedDateRangeDialogTitle": "Select date range", + "@extendedDateRangeDialogTitle": {}, + "extendedDateRangePickerAfterLabel": "After", "@extendedDateRangePickerAfterLabel": {}, - "extendedDateRangePickerBeforeLabel": "", + "extendedDateRangePickerBeforeLabel": "Before", "@extendedDateRangePickerBeforeLabel": {}, - "extendedDateRangePickerDayText": "{count, plural, other{}}", + "extendedDateRangePickerDayText": "{count, plural, zero{} one{day} other{days}}", "@extendedDateRangePickerDayText": { "placeholders": { "count": {} @@ -284,9 +298,9 @@ "count": {} } }, - "extendedDateRangePickerLastText": "", + "extendedDateRangePickerLastText": "Last", "@extendedDateRangePickerLastText": {}, - "extendedDateRangePickerLastWeeksLabel": "{count, plural, other{}}", + "extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}", "@extendedDateRangePickerLastWeeksLabel": { "placeholders": { "count": {} @@ -298,7 +312,7 @@ "count": {} } }, - "extendedDateRangePickerMonthText": "{count, plural, other{}}", + "extendedDateRangePickerMonthText": "{count, plural, zero{} one{month} other{months}}", "@extendedDateRangePickerMonthText": { "placeholders": { "count": {} @@ -306,13 +320,13 @@ }, "extendedDateRangePickerToLabel": "Do", "@extendedDateRangePickerToLabel": {}, - "extendedDateRangePickerWeekText": "{count, plural, other{}}", + "extendedDateRangePickerWeekText": "{count, plural, zero{} one{week} other{weeks}}", "@extendedDateRangePickerWeekText": { "placeholders": { "count": {} } }, - "extendedDateRangePickerYearText": "{count, plural, other{}}", + "extendedDateRangePickerYearText": "{count, plural, zero{} one{year} other{years}}", "@extendedDateRangePickerYearText": { "placeholders": { "count": {} @@ -424,8 +438,10 @@ "@loginPagePasswordValidatorMessageText": {}, "loginPageServerUrlFieldLabel": "'Adresa serveru", "@loginPageServerUrlFieldLabel": {}, - "loginPageServerUrlValidatorMessageText": "Adresa serveru nesmí být prázdná.", - "@loginPageServerUrlValidatorMessageText": {}, + "loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.", + "@loginPageServerUrlValidatorMessageInvalidAddressText": {}, + "loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.", + "@loginPageServerUrlValidatorMessageRequiredText": {}, "loginPageTitle": "Propojit s Paperless", "@loginPageTitle": {}, "loginPageUsernameLabel": "Jméno uživatele", @@ -499,5 +515,13 @@ "tagInboxTagPropertyLabel": "Tag inboxu", "@tagInboxTagPropertyLabel": {}, "uploadPageAutomaticallInferredFieldsHintText": "Pokud specifikuješ hodnoty pro tato pole, paperless instance nebude automaticky přiřazovat naučené hodnoty. Pokud mají být tato pole automaticky vyplňována, nevyplňujte zde nic.", - "@uploadPageAutomaticallInferredFieldsHintText": {} + "@uploadPageAutomaticallInferredFieldsHintText": {}, + "verifyIdentityPageDescriptionText": "Use the configured biometric factor to authenticate and unlock your documents.", + "@verifyIdentityPageDescriptionText": {}, + "verifyIdentityPageLogoutButtonLabel": "Disconnect", + "@verifyIdentityPageLogoutButtonLabel": {}, + "verifyIdentityPageTitle": "Verify your identity", + "@verifyIdentityPageTitle": {}, + "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index ed11e6b..028af80 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -260,6 +260,20 @@ "@errorMessageUnsupportedFileFormat": {}, "errorReportLabel": "MELDEN", "@errorReportLabel": {}, + "extendedDateRangeDialogAbsoluteLabel": "Absolut", + "@extendedDateRangeDialogAbsoluteLabel": {}, + "extendedDateRangeDialogHintText": "Hinweis: Neben konkreten Daten kannst du den Zeitraum auch über eine relative Zeitspanne einschränken.", + "@extendedDateRangeDialogHintText": {}, + "extendedDateRangeDialogRelativeAmountLabel": "Anzahl", + "@extendedDateRangeDialogRelativeAmountLabel": {}, + "extendedDateRangeDialogRelativeLabel": "Relativ", + "@extendedDateRangeDialogRelativeLabel": {}, + "extendedDateRangeDialogRelativeLastLabel": "Letzte", + "@extendedDateRangeDialogRelativeLastLabel": {}, + "extendedDateRangeDialogRelativeTimeUnitLabel": "Zeiteinheit", + "@extendedDateRangeDialogRelativeTimeUnitLabel": {}, + "extendedDateRangeDialogTitle": "Wähle Zeitraum", + "@extendedDateRangeDialogTitle": {}, "extendedDateRangePickerAfterLabel": "Nach", "@extendedDateRangePickerAfterLabel": {}, "extendedDateRangePickerBeforeLabel": "Vor", @@ -346,7 +360,7 @@ "@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {}, "inboxPageMarkAllAsSeenLabel": "Alle gesehen", "@inboxPageMarkAllAsSeenLabel": {}, - "inboxPageMarkAsSeenText": "Als gelesen markieren", + "inboxPageMarkAsSeenText": "Als gesehen markieren", "@inboxPageMarkAsSeenText": {}, "inboxPageNoNewDocumentsRefreshLabel": "Neu laden", "@inboxPageNoNewDocumentsRefreshLabel": {}, @@ -424,8 +438,10 @@ "@loginPagePasswordValidatorMessageText": {}, "loginPageServerUrlFieldLabel": "Server-Adresse", "@loginPageServerUrlFieldLabel": {}, - "loginPageServerUrlValidatorMessageText": "Server-Addresse darf nicht leer sein.", - "@loginPageServerUrlValidatorMessageText": {}, + "loginPageServerUrlValidatorMessageInvalidAddressText": "Ungültige Adresse.", + "@loginPageServerUrlValidatorMessageInvalidAddressText": {}, + "loginPageServerUrlValidatorMessageRequiredText": "Server-Addresse darf nicht leer sein.", + "@loginPageServerUrlValidatorMessageRequiredText": {}, "loginPageTitle": "Mit Paperless verbinden", "@loginPageTitle": {}, "loginPageUsernameLabel": "Nutzername", @@ -499,5 +515,13 @@ "tagInboxTagPropertyLabel": "Posteingangs-Tag", "@tagInboxTagPropertyLabel": {}, "uploadPageAutomaticallInferredFieldsHintText": "Wenn Werte für diese Felder angegeben werden, wird Paperless nicht automatisch einen Wert zuweisen. Wenn diese Felder automatisch von Paperless erkannt werden sollen, sollten die Felder leer bleiben.", - "@uploadPageAutomaticallInferredFieldsHintText": {} + "@uploadPageAutomaticallInferredFieldsHintText": {}, + "verifyIdentityPageDescriptionText": "Benutze den konfigurierten Biometrischen Faktor um dich zu identifizieren und auf deine Dokumente zuzugreifen.", + "@verifyIdentityPageDescriptionText": {}, + "verifyIdentityPageLogoutButtonLabel": "Verbindung trennen", + "@verifyIdentityPageLogoutButtonLabel": {}, + "verifyIdentityPageTitle": "Verifiziere deine Identität", + "@verifyIdentityPageTitle": {}, + "verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren", + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 92f41a5..b13a85d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -260,6 +260,20 @@ "@errorMessageUnsupportedFileFormat": {}, "errorReportLabel": "REPORT", "@errorReportLabel": {}, + "extendedDateRangeDialogAbsoluteLabel": "Absolute", + "@extendedDateRangeDialogAbsoluteLabel": {}, + "extendedDateRangeDialogHintText": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.", + "@extendedDateRangeDialogHintText": {}, + "extendedDateRangeDialogRelativeAmountLabel": "Amount", + "@extendedDateRangeDialogRelativeAmountLabel": {}, + "extendedDateRangeDialogRelativeLabel": "Relative", + "@extendedDateRangeDialogRelativeLabel": {}, + "extendedDateRangeDialogRelativeLastLabel": "Last", + "@extendedDateRangeDialogRelativeLastLabel": {}, + "extendedDateRangeDialogRelativeTimeUnitLabel": "Time unit", + "@extendedDateRangeDialogRelativeTimeUnitLabel": {}, + "extendedDateRangeDialogTitle": "Select date range", + "@extendedDateRangeDialogTitle": {}, "extendedDateRangePickerAfterLabel": "After", "@extendedDateRangePickerAfterLabel": {}, "extendedDateRangePickerBeforeLabel": "Before", @@ -424,8 +438,10 @@ "@loginPagePasswordValidatorMessageText": {}, "loginPageServerUrlFieldLabel": "Server Address", "@loginPageServerUrlFieldLabel": {}, - "loginPageServerUrlValidatorMessageText": "Server address must not be empty.", - "@loginPageServerUrlValidatorMessageText": {}, + "loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.", + "@loginPageServerUrlValidatorMessageInvalidAddressText": {}, + "loginPageServerUrlValidatorMessageRequiredText": "Server address must not be empty.", + "@loginPageServerUrlValidatorMessageRequiredText": {}, "loginPageTitle": "Connect to Paperless", "@loginPageTitle": {}, "loginPageUsernameLabel": "Username", @@ -499,5 +515,13 @@ "tagInboxTagPropertyLabel": "Inbox-Tag", "@tagInboxTagPropertyLabel": {}, "uploadPageAutomaticallInferredFieldsHintText": "If you specify values for these fields, your paperless instance will not automatically derive a value. If you want these values to be automatically populated by your server, leave the fields blank.", - "@uploadPageAutomaticallInferredFieldsHintText": {} + "@uploadPageAutomaticallInferredFieldsHintText": {}, + "verifyIdentityPageDescriptionText": "Use the configured biometric factor to authenticate and unlock your documents.", + "@verifyIdentityPageDescriptionText": {}, + "verifyIdentityPageLogoutButtonLabel": "Disconnect", + "@verifyIdentityPageLogoutButtonLabel": {}, + "verifyIdentityPageTitle": "Verify your identity", + "@verifyIdentityPageTitle": {}, + "verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity", + "@verifyIdentityPageVerifyIdentityButtonLabel": {} } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 336fb4a..09a8c5f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,6 +42,7 @@ import 'package:paperless_mobile/features/app_intro/application_intro_slideshow. 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'; import 'package:paperless_mobile/features/login/bloc/authentication_state.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart'; @@ -349,6 +350,9 @@ class _AuthenticationWrapperState extends State { ), ); if (success) { + Fluttertoast.showToast( + msg: S.of(context).documentUploadSuccessText, + ); SystemNavigator.pop(); } } @@ -397,7 +401,7 @@ class _AuthenticationWrapperState extends State { } else { if (authentication.wasLoginStored && !(authentication.wasLocalAuthenticationSuccessful ?? false)) { - return const BiometricAuthenticationPage(); + return const VerifyIdentityPage(); } return const LoginPage(); } @@ -406,50 +410,3 @@ class _AuthenticationWrapperState extends State { ); } } - -class BiometricAuthenticationPage extends StatelessWidget { - const BiometricAuthenticationPage({super.key}); - - @override - Widget build(BuildContext context) { - return Material( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "The app is locked!", - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - "You can now either try to authenticate again or disconnect from the current server.", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall, - ).padded(), - const SizedBox(height: 48), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ElevatedButton( - onPressed: () { - context.read().logout(); - context.read(); - HydratedBloc.storage.clear(); - }, - child: const Text("Log out"), - ), - ElevatedButton( - onPressed: () => context - .read() - .restoreSessionState(context - .read() - .state - .isLocalAuthenticationEnabled), - child: const Text("Authenticate"), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/util.dart b/lib/util.dart index 131d049..3af3b6f 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -35,8 +35,24 @@ void showSnackBar( ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text( - message + (details != null ? ' ($details)' : ''), + content: RichText( + maxLines: 5, + text: TextSpan( + text: message, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onInverseSurface, + ), + children: [ + if (details != null) + TextSpan( + text: "\n$details", + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 10, + ), + ), + ], + ), ), action: action != null ? SnackBarAction( 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 6e985a3..064decd 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,3 +1,5 @@ +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'; @@ -12,7 +14,6 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { required String username, required String password, }) async { - print(client.hashCode); late Response response; try { response = await client.post( @@ -29,7 +30,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { httpStatusCode: error.response?.statusCode, ); } else { - throw error.error; + log(error.message); + throw PaperlessServerException( + ErrorCode.authenticationFailed, + details: error.message, + ); } } diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart index 7c0fab8..097e9af 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart @@ -268,7 +268,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { data: path.toJson(), ); if (response.statusCode == HttpStatus.created) { - return StoragePath.fromJson(jsonDecode(response.data)); + return StoragePath.fromJson(response.data); } throw PaperlessServerException(ErrorCode.storagePathCreateFailed, httpStatusCode: response.statusCode); @@ -282,7 +282,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { data: path.toJson(), ); if (response.statusCode == HttpStatus.ok) { - return StoragePath.fromJson(jsonDecode(response.data)); + return StoragePath.fromJson(response.data); } throw const PaperlessServerException(ErrorCode.unknown); } diff --git a/pubspec.yaml b/pubspec.yaml index 3258ba5..50a8b87 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -83,7 +83,6 @@ dependencies: json_annotation: ^4.7.0 pretty_dio_logger: ^1.2.0-beta-1 - dev_dependencies: integration_test: sdk: flutter