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/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;
}

View File

@@ -46,7 +46,8 @@ class _ExtendedDateRangeDialogState extends State<ExtendedDateRangeDialog> {
@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<ExtendedDateRangeDialog> {
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<ExtendedDateRangeDialog> {
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},

View File

@@ -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,
),
),
),

View File

@@ -83,8 +83,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
-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<DocumentsPage> {
expand: false,
snap: true,
initialChildSize: .9,
maxChildSize: .9,
snapSizes: const [.9, 1],
builder: (context, controller) => LabelsBlocProvider(
child: DocumentFilterPanel(
initialFilter: context.read<DocumentsCubit>().state.filter,

View File

@@ -70,20 +70,19 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
resizeToAvoidBottomInset: true,
body: FormBuilder(
key: _formKey,
child: ListView(
child: _buildFormList(context),
),
),
);
}
ListView _buildFormList(BuildContext context) {
return 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),
),
),
child: _buildDragHandle(context),
),
Text(
S.of(context).documentFilterTitle,
@@ -123,8 +122,18 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
_buildStoragePathFormField().padded(),
_buildTagsFormField().padded(),
],
).paddedOnly(bottom: 16),
),
);
}
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),
),
);
}

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> {
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<ServerAddressFormField> {
}
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<ServerAddressFormField> {
setState(() => _reachabilityStatus = ReachabilityStatus.testing);
final isReachable = await context
.read<ConnectivityStatusService>()
.isServerReachable(address);
if (isReachable) {
setState(() => _reachabilityStatus = ReachabilityStatus.reachable);
} else {
setState(() => _reachabilityStatus = ReachabilityStatus.notReachable);
}
.isServerReachable(address.trim());
setState(
() => _reachabilityStatus = isReachable
? ReachabilityStatus.reachable
: ReachabilityStatus.notReachable,
);
}
}

View File

@@ -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": {}
}

View File

@@ -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": {}
}

View File

@@ -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": {}
}

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/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<AuthenticationWrapper> {
),
);
if (success) {
Fluttertoast.showToast(
msg: S.of(context).documentUploadSuccessText,
);
SystemNavigator.pop();
}
}
@@ -397,7 +401,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
} else {
if (authentication.wasLoginStored &&
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return const BiometricAuthenticationPage();
return const VerifyIdentityPage();
}
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()
..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: <TextSpan>[
if (details != null)
TextSpan(
text: "\n$details",
style: const TextStyle(
fontStyle: FontStyle.italic,
fontSize: 10,
),
),
],
),
),
action: action != null
? SnackBarAction(

View File

@@ -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,
);
}
}

View File

@@ -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);
}

View File

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