fix: Add custom fields, translations, add app logs to login routes

This commit is contained in:
Anton Stubenbord
2023-12-10 12:48:32 +01:00
parent 5e5e5d2df3
commit 9f6b95f506
102 changed files with 2399 additions and 1088 deletions

View File

@@ -112,6 +112,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async {
emit(const SwitchingAccountsState());
await FileService.instance.initialize();
final redactedId = redactUserId(localUserId);
logger.fd(
'Trying to switch to user $redactedId...',

View File

@@ -1,9 +1,13 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
@@ -13,10 +17,13 @@ import 'package:paperless_mobile/features/login/model/client_certificate_form_mo
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/login_settings_page.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/generated/assets.gen.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routing/routes/app_logs_route.dart';
class AddAccountPage extends StatefulWidget {
final FutureOr<void> Function(
@@ -58,10 +65,172 @@ class _AddAccountPageState extends State<AddAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
bool _isCheckingConnection = false;
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
bool _isFormSubmitted = false;
final _pageController = PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(widget.titleText),
),
body: FormBuilder(
key: _formKey,
child: AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Assets.logos.paperlessLogoGreenPng.image(
width: 150,
height: 150,
),
Text(
'Paperless Mobile',
style: Theme.of(context).textTheme.displaySmall,
).padded(),
SizedBox(height: 24),
Expanded(
child: PageView(
physics: NeverScrollableScrollPhysics(),
controller: _pageController,
allowImplicitScrolling: false,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ServerAddressFormField(
onChanged: (value) {
setState(() {
_reachabilityStatus = ReachabilityStatus.unknown;
});
},
).paddedSymmetrically(
horizontal: 12,
vertical: 12,
),
ClientCertificateFormField(
initialBytes: widget.initialClientCertificate?.bytes,
initialPassphrase:
widget.initialClientCertificate?.passphrase,
).padded(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
//TODO: Move additional headers and client cert to separate page
// IconButton.filledTonal(
// onPressed: () {
// Navigator.of(context).push(
// MaterialPageRoute(builder: (context) {
// return LoginSettingsPage();
// }),
// );
// },
// icon: Icon(Icons.settings),
// ),
SizedBox(width: 8),
FilledButton.icon(
onPressed: () async {
final status = await _updateReachability();
if (status == ReachabilityStatus.reachable) {
Future.delayed(1.seconds, () {
_pageController.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
}
},
icon: _isCheckingConnection
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context)
.colorScheme
.onSecondary,
),
)
: _reachabilityStatus ==
ReachabilityStatus.reachable
? Icon(Icons.done)
: Icon(Icons.arrow_forward),
label: Text(S.of(context)!.continueLabel),
),
],
).paddedSymmetrically(
horizontal: 16,
vertical: 8,
),
_buildStatusIndicator().padded(),
],
),
Column(
children: [
UserCredentialsFormField(
formKey: _formKey,
initialUsername: widget.initialUsername,
initialPassword: widget.initialPassword,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
_pageController.previousPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
icon: Icon(Icons.arrow_back),
label: Text(S.of(context)!.edit),
),
FilledButton(
onPressed: () {
_onSubmit();
},
child: Text(S.of(context)!.signIn),
),
],
).padded(),
Text(
S.of(context)!.loginRequiredPermissionsHint,
style: Theme.of(context).textTheme.bodySmall?.apply(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.6),
),
).padded(16),
],
),
],
),
),
Text.rich(
TextSpan(
style: Theme.of(context).textTheme.labelLarge,
children: [
TextSpan(text: S.of(context)!.version(packageInfo.version)),
WidgetSpan(child: SizedBox(width: 24)),
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
text: S.of(context)!.appLogs(''),
recognizer: TapGestureRecognizer()
..onTap = () {
AppLogsRoute().push(context);
},
),
],
),
).padded(),
],
),
),
),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.titleText),
@@ -91,7 +260,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
children: [
ServerAddressFormField(
initialValue: widget.initialServerUrl,
onSubmit: (address) {
onChanged: (address) {
_updateReachability(address);
},
).padded(),
@@ -117,7 +286,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
.withOpacity(0.6),
),
).padded(16),
]
],
],
),
),
@@ -125,7 +294,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
);
}
Future<void> _updateReachability([String? address]) async {
Future<ReachabilityStatus> _updateReachability([String? address]) async {
setState(() {
_isCheckingConnection = true;
});
@@ -150,13 +319,10 @@ class _AddAccountPageState extends State<AddAccountPage> {
_isCheckingConnection = false;
_reachabilityStatus = status;
});
return status;
}
Widget _buildStatusIndicator() {
if (_isCheckingConnection) {
return const ListTile();
}
Widget _buildIconText(
IconData icon,
String text, [
@@ -176,14 +342,6 @@ class _AddAccountPageState extends State<AddAccountPage> {
Color errorColor = Theme.of(context).colorScheme.error;
switch (_reachabilityStatus) {
case ReachabilityStatus.unknown:
return Container();
case ReachabilityStatus.reachable:
return _buildIconText(
Icons.done,
S.of(context)!.connectionSuccessfulylEstablished,
Colors.green,
);
case ReachabilityStatus.notReachable:
return _buildIconText(
Icons.close,
@@ -214,6 +372,8 @@ class _AddAccountPageState extends State<AddAccountPage> {
S.of(context)!.connectionTimedOut,
errorColor,
);
default:
return const ListTile();
}
}

View File

@@ -7,7 +7,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:path/path.dart' as p;
import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget {
@@ -16,10 +17,10 @@ class ClientCertificateFormField extends StatefulWidget {
final String? initialPassphrase;
final Uint8List? initialBytes;
final void Function(ClientCertificateFormModel? cert) onChanged;
final ValueChanged<ClientCertificateFormModel?>? onChanged;
const ClientCertificateFormField({
super.key,
required this.onChanged,
this.onChanged,
this.initialPassphrase,
this.initialBytes,
});
@@ -29,13 +30,15 @@ class ClientCertificateFormField extends StatefulWidget {
_ClientCertificateFormFieldState();
}
class _ClientCertificateFormFieldState
extends State<ClientCertificateFormField> {
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
with AutomaticKeepAliveClientMixin {
File? _selectedFile;
@override
Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<ClientCertificateFormModel?>(
key: const ValueKey('login-client-cert'),
name: ClientCertificateFormField.fkClientCertificate,
onChanged: widget.onChanged,
initialValue: widget.initialBytes != null
? ClientCertificateFormModel(
@@ -43,16 +46,6 @@ class _ClientCertificateFormFieldState
passphrase: widget.initialPassphrase,
)
: null,
validator: (value) {
if (value == null) {
return null;
}
assert(_selectedFile != null);
if (_selectedFile?.path.split(".").last != 'pfx') {
return S.of(context)!.invalidCertificateFormat;
}
return null;
},
builder: (field) {
final theme =
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
@@ -127,7 +120,6 @@ class _ClientCertificateFormFieldState
),
);
},
name: ClientCertificateFormField.fkClientCertificate,
);
}
@@ -140,6 +132,11 @@ class _ClientCertificateFormFieldState
if (result == null || result.files.single.path == null) {
return;
}
final path = result.files.single.path!;
if (p.extension(path) != '.pfx') {
showSnackBar(context, S.of(context)!.invalidCertificateFormat);
return;
}
File file = File(result.files.single.path!);
setState(() {
_selectedFile = file;
@@ -171,4 +168,7 @@ class _ClientCertificateFormFieldState
);
}
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class LoginSettingsPage extends StatelessWidget {
const LoginSettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.settings),
),
body: ListView(
children: [
ClientCertificateFormField(onChanged: (certificate) {}),
],
),
);
}
}

View File

@@ -9,10 +9,11 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class ServerAddressFormField extends StatefulWidget {
static const String fkServerAddress = "serverAddress";
final String? initialValue;
final void Function(String? address) onSubmit;
final ValueChanged<String?>? onChanged;
const ServerAddressFormField({
Key? key,
required this.onSubmit,
this.onChanged,
this.initialValue,
}) : super(key: key);
@@ -20,8 +21,10 @@ class ServerAddressFormField extends StatefulWidget {
State<ServerAddressFormField> createState() => _ServerAddressFormFieldState();
}
class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
class _ServerAddressFormFieldState extends State<ServerAddressFormField>
with AutomaticKeepAliveClientMixin {
bool _canClear = false;
final _textFieldKey = GlobalKey();
@override
void initState() {
@@ -38,10 +41,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
@override
Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<String>(
initialValue: widget.initialValue,
name: ServerAddressFormField.fkServerAddress,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: widget.onChanged,
builder: (field) {
return RawAutocomplete<String>(
focusNode: _focusNode,
@@ -51,6 +56,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
onSelected: onSelected,
options: options,
maxOptionsHeight: 200.0,
maxWidth: MediaQuery.sizeOf(context).width - 40,
);
},
key: const ValueKey('login-server-address'),
@@ -60,12 +66,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
.where((element) => element.contains(textEditingValue.text));
},
onSelected: (option) {
_formatInput();
field.didChange(_textEditingController.text);
_formatInput(field);
},
fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) {
return TextFormField(
key: _textFieldKey,
controller: textEditingController,
focusNode: focusNode,
decoration: InputDecoration(
@@ -78,15 +84,22 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
onPressed: () {
textEditingController.clear();
field.didChange(textEditingController.text);
widget.onSubmit(textEditingController.text);
},
)
: null,
),
autofocus: false,
onFieldSubmitted: (_) {
_formatInput(field);
onFieldSubmitted();
_formatInput();
},
onTapOutside: (event) {
if (!FocusScope.of(context).hasFocus) {
return;
}
_formatInput(field);
onFieldSubmitted();
FocusScope.of(context).unfocus();
},
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
@@ -113,7 +126,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
);
}
void _formatInput() {
void _formatInput(FormFieldState<String> field) {
String address = _textEditingController.text.trim();
address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
_textEditingController.text = address;
@@ -121,8 +134,11 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
baseOffset: address.length,
extentOffset: address.length,
);
widget.onSubmit(address);
field.didChange(_textEditingController.text);
}
@override
bool get wantKeepAlive => true;
}
/// Taken from [Autocomplete]
@@ -131,12 +147,14 @@ class _AutocompleteOptions extends StatelessWidget {
required this.onSelected,
required this.options,
required this.maxOptionsHeight,
required this.maxWidth,
});
final AutocompleteOnSelected<String> onSelected;
final Iterable<String> options;
final double maxOptionsHeight;
final double maxWidth;
@override
Widget build(BuildContext context) {
@@ -145,7 +163,10 @@ class _AutocompleteOptions extends StatelessWidget {
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
constraints: BoxConstraints(
maxHeight: maxOptionsHeight,
maxWidth: maxWidth,
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,

View File

@@ -12,13 +12,13 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class UserCredentialsFormField extends StatefulWidget {
static const fkCredentials = 'credentials';
final void Function() onFieldsSubmitted;
final VoidCallback? onFieldsSubmitted;
final String? initialUsername;
final String? initialPassword;
final GlobalKey<FormBuilderState> formKey;
const UserCredentialsFormField({
Key? key,
required this.onFieldsSubmitted,
this.onFieldsSubmitted,
this.initialUsername,
this.initialPassword,
required this.formKey,
@@ -29,12 +29,14 @@ class UserCredentialsFormField extends StatefulWidget {
_UserCredentialsFormFieldState();
}
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField>
with AutomaticKeepAliveClientMixin {
final _usernameFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
@override
Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<LoginFormCredentials?>(
initialValue: LoginFormCredentials(
password: widget.initialPassword,
@@ -87,7 +89,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
LoginFormCredentials(password: password),
),
onFieldSubmitted: (_) {
widget.onFieldsSubmitted();
widget.onFieldsSubmitted?.call();
},
validator: (value) {
if (value?.trim().isEmpty ?? true) {
@@ -100,6 +102,9 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
),
);
}
@override
bool get wantKeepAlive => true;
}
/**

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routing/routes/app_logs_route.dart';
import 'package:paperless_mobile/theme.dart';
class LoginTransitionPage extends StatelessWidget {
@@ -20,10 +22,25 @@ class LoginTransitionPage extends StatelessWidget {
body: Stack(
alignment: Alignment.center,
children: [
const CircularProgressIndicator(),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Align(
alignment: Alignment.bottomCenter,
child: Text(text).paddedOnly(bottom: 24),
),
],
),
Align(
alignment: Alignment.bottomCenter,
child: Text(text).paddedOnly(bottom: 24),
child: TextButton(
child: Text(S.of(context)!.appLogs('')),
onPressed: () {
AppLogsRoute().push(context);
},
),
),
],
).padded(16),