Added server address validation, success message, localization

This commit is contained in:
Anton Stubenbord
2022-12-31 01:35:26 +01:00
parent 2326c9d1d6
commit f390aa6c6a
15 changed files with 315 additions and 151 deletions

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dio/adapter.dart'; import 'package:dio/adapter.dart';
import 'package:dio/dio.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/extensions/security_context_extension.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
@@ -23,6 +24,7 @@ class AuthenticationAwareDioManager {
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) => client..badCertificateCallback = (cert, host, port) => true; (client) => client..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll(interceptors); dio.interceptors.addAll(interceptors);
dio.interceptors.add(RetryOnConnectionChangeInterceptor(dio: dio));
return dio; return dio;
} }

View File

@@ -46,7 +46,8 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text("Select date range"), insetPadding: const EdgeInsets.all(24.0),
title: Text(S.of(context).extendedDateRangeDialogTitle),
content: FormBuilder( content: FormBuilder(
key: _formKey, key: _formKey,
child: Column( child: Column(
@@ -55,7 +56,7 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
children: [ children: [
_buildDateRangeQueryTypeSelection(), _buildDateRangeQueryTypeSelection(),
Text( 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, style: Theme.of(context).textTheme.bodySmall,
).paddedOnly(top: 8, bottom: 16), ).paddedOnly(top: 8, bottom: 16),
Builder( Builder(
@@ -109,12 +110,12 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
ButtonSegment( ButtonSegment(
value: DateRangeType.absolute, value: DateRangeType.absolute,
enabled: true, enabled: true,
label: Text("Absolute"), label: Text(S.of(context).extendedDateRangeDialogAbsoluteLabel),
), ),
ButtonSegment( ButtonSegment(
value: DateRangeType.relative, value: DateRangeType.relative,
enabled: true, enabled: true,
label: Text("Relative"), label: Text(S.of(context).extendedDateRangeDialogRelativeLabel),
), ),
], ],
selected: {_selectedDateRangeType}, selected: {_selectedDateRangeType},

View File

@@ -47,12 +47,14 @@ class _FormBuilderRelativeDateRangePickerState
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("Last"), Text(S.of(context).extendedDateRangeDialogRelativeLastLabel),
SizedBox( SizedBox(
width: 70, width: 80,
child: TextFormField( child: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Offset", labelText: S
.of(context)
.extendedDateRangeDialogRelativeAmountLabel,
), ),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.digitsOnly, FilteringTextInputFormatter.digitsOnly,
@@ -91,7 +93,9 @@ class _FormBuilderRelativeDateRangePickerState
onChanged: (value) => onChanged: (value) =>
field.didChange(field.value!.copyWith(unit: value)), field.didChange(field.value!.copyWith(unit: value)),
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Amount", labelText: S
.of(context)
.extendedDateRangeDialogRelativeTimeUnitLabel,
), ),
), ),
), ),

View File

@@ -83,8 +83,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
-4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd -4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
isLabelVisible: appliedFiltersCount > 0, isLabelVisible: appliedFiltersCount > 0,
count: state.filter.appliedFiltersCount, count: state.filter.appliedFiltersCount,
backgroundColor: Theme.of(context).colorScheme.errorContainer, backgroundColor: Colors.red,
textColor: Theme.of(context).colorScheme.onErrorContainer, textColor: Colors.white,
child: FloatingActionButton( child: FloatingActionButton(
child: const Icon(Icons.filter_alt_outlined), child: const Icon(Icons.filter_alt_outlined),
onPressed: _openDocumentFilter, onPressed: _openDocumentFilter,
@@ -115,7 +115,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
expand: false, expand: false,
snap: true, snap: true,
initialChildSize: .9, initialChildSize: .9,
maxChildSize: .9, snapSizes: const [.9, 1],
builder: (context, controller) => LabelsBlocProvider( builder: (context, controller) => LabelsBlocProvider(
child: DocumentFilterPanel( child: DocumentFilterPanel(
initialFilter: context.read<DocumentsCubit>().state.filter, initialFilter: context.read<DocumentsCubit>().state.filter,

View File

@@ -70,65 +70,74 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
body: FormBuilder( body: FormBuilder(
key: _formKey, key: _formKey,
child: ListView( child: _buildFormList(context),
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),
), ),
), ),
); );
} }
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<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() { BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>( return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) { builder: (context, state) {

View File

@@ -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<AuthenticationCubit>()
.restoreSessionState(context
.read<ApplicationSettingsCubit>()
.state
.isLocalAuthenticationEnabled),
child: Text(S
.of(context)
.verifyIdentityPageVerifyIdentityButtonLabel),
),
],
).padded(16),
],
),
),
);
}
void _logout(BuildContext context) {
context.read<AuthenticationCubit>().logout();
context.read<LabelRepository<Tag>>().clear();
context.read<LabelRepository<Correspondent>>().clear();
context.read<LabelRepository<DocumentType>>().clear();
context.read<LabelRepository<StoragePath>>().clear();
context.read<SavedViewRepository>().clear();
HydratedBloc.storage.clear();
}
}

View File

@@ -17,26 +17,49 @@ class ServerAddressFormField extends StatefulWidget {
} }
class _ServerAddressFormFieldState extends State<ServerAddressFormField> { class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
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; ReachabilityStatus _reachabilityStatus = ReachabilityStatus.undefined;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FormBuilderTextField( return FormBuilderTextField(
key: const ValueKey('login-server-address'), key: const ValueKey('login-server-address'),
controller: _textEditingController,
name: ServerAddressFormField.fkServerAddress, name: ServerAddressFormField.fkServerAddress,
validator: FormBuilderValidators.required( validator: FormBuilderValidators.compose(
errorText: S.of(context).loginPageServerUrlValidatorMessageText, [
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( decoration: InputDecoration(
suffixIcon: _buildIsReachableIcon(), suffixIcon: _buildIsReachableIcon(),
hintText: "http://192.168.1.50:8000", hintText: "http://192.168.1.50:8000",
labelText: S.of(context).loginPageServerUrlFieldLabel, labelText: S.of(context).loginPageServerUrlFieldLabel,
), ),
onChanged: _updateIsAddressReachableStatus, 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<ServerAddressFormField> {
} }
void _updateIsAddressReachableStatus(String? address) async { void _updateIsAddressReachableStatus(String? address) async {
if (address == null || address.isEmpty) { if (address == null || !_urlRegex.hasMatch(address)) {
setState(() { setState(() {
_reachabilityStatus = ReachabilityStatus.undefined; _reachabilityStatus = ReachabilityStatus.undefined;
}); });
@@ -70,12 +93,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
setState(() => _reachabilityStatus = ReachabilityStatus.testing); setState(() => _reachabilityStatus = ReachabilityStatus.testing);
final isReachable = await context final isReachable = await context
.read<ConnectivityStatusService>() .read<ConnectivityStatusService>()
.isServerReachable(address); .isServerReachable(address.trim());
if (isReachable) { setState(
setState(() => _reachabilityStatus = ReachabilityStatus.reachable); () => _reachabilityStatus = isReachable
} else { ? ReachabilityStatus.reachable
setState(() => _reachabilityStatus = ReachabilityStatus.notReachable); : ReachabilityStatus.notReachable,
} );
} }
} }

View File

@@ -260,11 +260,25 @@
"@errorMessageUnsupportedFileFormat": {}, "@errorMessageUnsupportedFileFormat": {},
"errorReportLabel": "NAHLÁSIT", "errorReportLabel": "NAHLÁSIT",
"@errorReportLabel": {}, "@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": {}, "@extendedDateRangePickerAfterLabel": {},
"extendedDateRangePickerBeforeLabel": "", "extendedDateRangePickerBeforeLabel": "Before",
"@extendedDateRangePickerBeforeLabel": {}, "@extendedDateRangePickerBeforeLabel": {},
"extendedDateRangePickerDayText": "{count, plural, other{}}", "extendedDateRangePickerDayText": "{count, plural, zero{} one{day} other{days}}",
"@extendedDateRangePickerDayText": { "@extendedDateRangePickerDayText": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@@ -284,9 +298,9 @@
"count": {} "count": {}
} }
}, },
"extendedDateRangePickerLastText": "", "extendedDateRangePickerLastText": "Last",
"@extendedDateRangePickerLastText": {}, "@extendedDateRangePickerLastText": {},
"extendedDateRangePickerLastWeeksLabel": "{count, plural, other{}}", "extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}",
"@extendedDateRangePickerLastWeeksLabel": { "@extendedDateRangePickerLastWeeksLabel": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@@ -298,7 +312,7 @@
"count": {} "count": {}
} }
}, },
"extendedDateRangePickerMonthText": "{count, plural, other{}}", "extendedDateRangePickerMonthText": "{count, plural, zero{} one{month} other{months}}",
"@extendedDateRangePickerMonthText": { "@extendedDateRangePickerMonthText": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@@ -306,13 +320,13 @@
}, },
"extendedDateRangePickerToLabel": "Do", "extendedDateRangePickerToLabel": "Do",
"@extendedDateRangePickerToLabel": {}, "@extendedDateRangePickerToLabel": {},
"extendedDateRangePickerWeekText": "{count, plural, other{}}", "extendedDateRangePickerWeekText": "{count, plural, zero{} one{week} other{weeks}}",
"@extendedDateRangePickerWeekText": { "@extendedDateRangePickerWeekText": {
"placeholders": { "placeholders": {
"count": {} "count": {}
} }
}, },
"extendedDateRangePickerYearText": "{count, plural, other{}}", "extendedDateRangePickerYearText": "{count, plural, zero{} one{year} other{years}}",
"@extendedDateRangePickerYearText": { "@extendedDateRangePickerYearText": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@@ -424,8 +438,10 @@
"@loginPagePasswordValidatorMessageText": {}, "@loginPagePasswordValidatorMessageText": {},
"loginPageServerUrlFieldLabel": "'Adresa serveru", "loginPageServerUrlFieldLabel": "'Adresa serveru",
"@loginPageServerUrlFieldLabel": {}, "@loginPageServerUrlFieldLabel": {},
"loginPageServerUrlValidatorMessageText": "Adresa serveru nesmí být prázdná.", "loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
"@loginPageServerUrlValidatorMessageText": {}, "@loginPageServerUrlValidatorMessageInvalidAddressText": {},
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
"@loginPageServerUrlValidatorMessageRequiredText": {},
"loginPageTitle": "Propojit s Paperless", "loginPageTitle": "Propojit s Paperless",
"@loginPageTitle": {}, "@loginPageTitle": {},
"loginPageUsernameLabel": "Jméno uživatele", "loginPageUsernameLabel": "Jméno uživatele",
@@ -499,5 +515,13 @@
"tagInboxTagPropertyLabel": "Tag inboxu", "tagInboxTagPropertyLabel": "Tag inboxu",
"@tagInboxTagPropertyLabel": {}, "@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": "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": {}
} }

View File

@@ -260,6 +260,20 @@
"@errorMessageUnsupportedFileFormat": {}, "@errorMessageUnsupportedFileFormat": {},
"errorReportLabel": "MELDEN", "errorReportLabel": "MELDEN",
"@errorReportLabel": {}, "@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": "Nach",
"@extendedDateRangePickerAfterLabel": {}, "@extendedDateRangePickerAfterLabel": {},
"extendedDateRangePickerBeforeLabel": "Vor", "extendedDateRangePickerBeforeLabel": "Vor",
@@ -346,7 +360,7 @@
"@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {}, "@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {},
"inboxPageMarkAllAsSeenLabel": "Alle gesehen", "inboxPageMarkAllAsSeenLabel": "Alle gesehen",
"@inboxPageMarkAllAsSeenLabel": {}, "@inboxPageMarkAllAsSeenLabel": {},
"inboxPageMarkAsSeenText": "Als gelesen markieren", "inboxPageMarkAsSeenText": "Als gesehen markieren",
"@inboxPageMarkAsSeenText": {}, "@inboxPageMarkAsSeenText": {},
"inboxPageNoNewDocumentsRefreshLabel": "Neu laden", "inboxPageNoNewDocumentsRefreshLabel": "Neu laden",
"@inboxPageNoNewDocumentsRefreshLabel": {}, "@inboxPageNoNewDocumentsRefreshLabel": {},
@@ -424,8 +438,10 @@
"@loginPagePasswordValidatorMessageText": {}, "@loginPagePasswordValidatorMessageText": {},
"loginPageServerUrlFieldLabel": "Server-Adresse", "loginPageServerUrlFieldLabel": "Server-Adresse",
"@loginPageServerUrlFieldLabel": {}, "@loginPageServerUrlFieldLabel": {},
"loginPageServerUrlValidatorMessageText": "Server-Addresse darf nicht leer sein.", "loginPageServerUrlValidatorMessageInvalidAddressText": "Ungültige Adresse.",
"@loginPageServerUrlValidatorMessageText": {}, "@loginPageServerUrlValidatorMessageInvalidAddressText": {},
"loginPageServerUrlValidatorMessageRequiredText": "Server-Addresse darf nicht leer sein.",
"@loginPageServerUrlValidatorMessageRequiredText": {},
"loginPageTitle": "Mit Paperless verbinden", "loginPageTitle": "Mit Paperless verbinden",
"@loginPageTitle": {}, "@loginPageTitle": {},
"loginPageUsernameLabel": "Nutzername", "loginPageUsernameLabel": "Nutzername",
@@ -499,5 +515,13 @@
"tagInboxTagPropertyLabel": "Posteingangs-Tag", "tagInboxTagPropertyLabel": "Posteingangs-Tag",
"@tagInboxTagPropertyLabel": {}, "@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": "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": {}
} }

View File

@@ -260,6 +260,20 @@
"@errorMessageUnsupportedFileFormat": {}, "@errorMessageUnsupportedFileFormat": {},
"errorReportLabel": "REPORT", "errorReportLabel": "REPORT",
"@errorReportLabel": {}, "@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": "After",
"@extendedDateRangePickerAfterLabel": {}, "@extendedDateRangePickerAfterLabel": {},
"extendedDateRangePickerBeforeLabel": "Before", "extendedDateRangePickerBeforeLabel": "Before",
@@ -424,8 +438,10 @@
"@loginPagePasswordValidatorMessageText": {}, "@loginPagePasswordValidatorMessageText": {},
"loginPageServerUrlFieldLabel": "Server Address", "loginPageServerUrlFieldLabel": "Server Address",
"@loginPageServerUrlFieldLabel": {}, "@loginPageServerUrlFieldLabel": {},
"loginPageServerUrlValidatorMessageText": "Server address must not be empty.", "loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
"@loginPageServerUrlValidatorMessageText": {}, "@loginPageServerUrlValidatorMessageInvalidAddressText": {},
"loginPageServerUrlValidatorMessageRequiredText": "Server address must not be empty.",
"@loginPageServerUrlValidatorMessageRequiredText": {},
"loginPageTitle": "Connect to Paperless", "loginPageTitle": "Connect to Paperless",
"@loginPageTitle": {}, "@loginPageTitle": {},
"loginPageUsernameLabel": "Username", "loginPageUsernameLabel": "Username",
@@ -499,5 +515,13 @@
"tagInboxTagPropertyLabel": "Inbox-Tag", "tagInboxTagPropertyLabel": "Inbox-Tag",
"@tagInboxTagPropertyLabel": {}, "@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": "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": {}
} }

View File

@@ -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/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.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/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_cubit.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
@@ -349,6 +350,9 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
), ),
); );
if (success) { if (success) {
Fluttertoast.showToast(
msg: S.of(context).documentUploadSuccessText,
);
SystemNavigator.pop(); SystemNavigator.pop();
} }
} }
@@ -397,7 +401,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
} else { } else {
if (authentication.wasLoginStored && if (authentication.wasLoginStored &&
!(authentication.wasLocalAuthenticationSuccessful ?? false)) { !(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return const BiometricAuthenticationPage(); return const VerifyIdentityPage();
} }
return const LoginPage(); return const LoginPage();
} }
@@ -406,50 +410,3 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
); );
} }
} }
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<AuthenticationCubit>().logout();
context.read();
HydratedBloc.storage.clear();
},
child: const Text("Log out"),
),
ElevatedButton(
onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(context
.read<ApplicationSettingsCubit>()
.state
.isLocalAuthenticationEnabled),
child: const Text("Authenticate"),
),
],
),
],
),
);
}
}

View File

@@ -35,8 +35,24 @@ void showSnackBar(
..hideCurrentSnackBar() ..hideCurrentSnackBar()
..showSnackBar( ..showSnackBar(
SnackBar( SnackBar(
content: Text( content: RichText(
message + (details != null ? ' ($details)' : ''), maxLines: 5,
text: TextSpan(
text: message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onInverseSurface,
),
children: <TextSpan>[
if (details != null)
TextSpan(
text: "\n$details",
style: const TextStyle(
fontStyle: FontStyle.italic,
fontSize: 10,
),
),
],
),
), ),
action: action != null action: action != null
? SnackBarAction( ? SnackBarAction(

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart'; import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart';
@@ -12,7 +14,6 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
required String username, required String username,
required String password, required String password,
}) async { }) async {
print(client.hashCode);
late Response response; late Response response;
try { try {
response = await client.post( response = await client.post(
@@ -29,7 +30,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
httpStatusCode: error.response?.statusCode, httpStatusCode: error.response?.statusCode,
); );
} else { } else {
throw error.error; log(error.message);
throw PaperlessServerException(
ErrorCode.authenticationFailed,
details: error.message,
);
} }
} }

View File

@@ -268,7 +268,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
data: path.toJson(), data: path.toJson(),
); );
if (response.statusCode == HttpStatus.created) { if (response.statusCode == HttpStatus.created) {
return StoragePath.fromJson(jsonDecode(response.data)); return StoragePath.fromJson(response.data);
} }
throw PaperlessServerException(ErrorCode.storagePathCreateFailed, throw PaperlessServerException(ErrorCode.storagePathCreateFailed,
httpStatusCode: response.statusCode); httpStatusCode: response.statusCode);
@@ -282,7 +282,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
data: path.toJson(), data: path.toJson(),
); );
if (response.statusCode == HttpStatus.ok) { if (response.statusCode == HttpStatus.ok) {
return StoragePath.fromJson(jsonDecode(response.data)); return StoragePath.fromJson(response.data);
} }
throw const PaperlessServerException(ErrorCode.unknown); throw const PaperlessServerException(ErrorCode.unknown);
} }

View File

@@ -83,7 +83,6 @@ dependencies:
json_annotation: ^4.7.0 json_annotation: ^4.7.0
pretty_dio_logger: ^1.2.0-beta-1 pretty_dio_logger: ^1.2.0-beta-1
dev_dependencies: dev_dependencies:
integration_test: integration_test:
sdk: flutter sdk: flutter