mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-15 04:12:30 -06:00
feat: bugfixes, finished go_router migration, implemented better visibility of states
This commit is contained in:
@@ -3,27 +3,21 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.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/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||
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/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
|
||||
import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
|
||||
import 'widgets/login_pages/server_login_page.dart';
|
||||
import 'widgets/never_scrollable_scroll_behavior.dart';
|
||||
|
||||
class AddAccountPage extends StatefulWidget {
|
||||
final FutureOr<void> Function(
|
||||
BuildContext context,
|
||||
@@ -33,17 +27,27 @@ class AddAccountPage extends StatefulWidget {
|
||||
ClientCertificate? clientCertificate,
|
||||
) onSubmit;
|
||||
|
||||
final String submitText;
|
||||
final String titleString;
|
||||
final String? initialServerUrl;
|
||||
final String? initialUsername;
|
||||
final String? initialPassword;
|
||||
final ClientCertificate? initialClientCertificate;
|
||||
|
||||
final String submitText;
|
||||
final String titleText;
|
||||
final bool showLocalAccounts;
|
||||
|
||||
final Widget? bottomLeftButton;
|
||||
const AddAccountPage({
|
||||
Key? key,
|
||||
required this.onSubmit,
|
||||
required this.submitText,
|
||||
required this.titleString,
|
||||
required this.titleText,
|
||||
this.showLocalAccounts = false,
|
||||
this.initialServerUrl,
|
||||
this.initialUsername,
|
||||
this.initialPassword,
|
||||
this.initialClientCertificate,
|
||||
this.bottomLeftButton,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -52,86 +56,170 @@ class AddAccountPage extends StatefulWidget {
|
||||
|
||||
class _AddAccountPageState extends State<AddAccountPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
bool _isCheckingConnection = false;
|
||||
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
|
||||
|
||||
final PageController _pageController = PageController();
|
||||
|
||||
bool _isFormSubmitted = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
||||
builder: (context, localAccounts, child) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
scrollBehavior: NeverScrollableScrollBehavior(),
|
||||
children: [
|
||||
if (widget.showLocalAccounts && localAccounts.isNotEmpty)
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context)!.logInToExistingAccount),
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton(
|
||||
child: Text(S.of(context)!.goToLogin),
|
||||
onPressed: () {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final account = localAccounts.values.elementAt(index);
|
||||
return Card(
|
||||
child: UserAccountListTile(
|
||||
account: account,
|
||||
onTap: () {
|
||||
context
|
||||
.read<AuthenticationCubit>()
|
||||
.switchAccount(account.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: localAccounts.length,
|
||||
),
|
||||
),
|
||||
ServerConnectionPage(
|
||||
titleText: widget.titleString,
|
||||
formBuilderKey: _formKey,
|
||||
onContinue: () {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
),
|
||||
ServerLoginPage(
|
||||
formBuilderKey: _formKey,
|
||||
submitText: widget.submitText,
|
||||
onSubmit: _login,
|
||||
),
|
||||
],
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.titleText),
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.bottomLeftButton != null
|
||||
? MainAxisAlignment.spaceBetween
|
||||
: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (widget.bottomLeftButton != null) widget.bottomLeftButton!,
|
||||
FilledButton(
|
||||
child: Text(S.of(context)!.loginPageSignInTitle),
|
||||
onPressed: _reachabilityStatus == ReachabilityStatus.reachable &&
|
||||
!_isFormSubmitted
|
||||
? _onSubmit
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
ServerAddressFormField(
|
||||
initialValue: widget.initialServerUrl,
|
||||
onSubmit: (address) {
|
||||
_updateReachability(address);
|
||||
},
|
||||
).padded(),
|
||||
ClientCertificateFormField(
|
||||
initialBytes: widget.initialClientCertificate?.bytes,
|
||||
initialPassphrase: widget.initialClientCertificate?.passphrase,
|
||||
onChanged: (_) => _updateReachability(),
|
||||
).padded(),
|
||||
_buildStatusIndicator(),
|
||||
if (_reachabilityStatus == ReachabilityStatus.reachable) ...[
|
||||
UserCredentialsFormField(
|
||||
formKey: _formKey,
|
||||
initialUsername: widget.initialUsername,
|
||||
initialPassword: widget.initialPassword,
|
||||
onFieldsSubmitted: _onSubmit,
|
||||
),
|
||||
Text(
|
||||
S.of(context)!.loginRequiredPermissionsHint,
|
||||
style: Theme.of(context).textTheme.bodySmall?.apply(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
).padded(16),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
Future<void> _updateReachability([String? address]) async {
|
||||
setState(() {
|
||||
_isCheckingConnection = true;
|
||||
});
|
||||
final certForm =
|
||||
_formKey.currentState?.getRawValue<ClientCertificateFormModel>(
|
||||
ClientCertificateFormField.fkClientCertificate,
|
||||
);
|
||||
final status = await context
|
||||
.read<ConnectivityStatusService>()
|
||||
.isPaperlessServerReachable(
|
||||
address ??
|
||||
_formKey.currentState!
|
||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||
certForm != null
|
||||
? ClientCertificate(
|
||||
bytes: certForm.bytes,
|
||||
passphrase: certForm.passphrase,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
setState(() {
|
||||
_isCheckingConnection = false;
|
||||
_reachabilityStatus = status;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicator() {
|
||||
if (_isCheckingConnection) {
|
||||
return const ListTile();
|
||||
}
|
||||
|
||||
Widget _buildIconText(
|
||||
IconData icon,
|
||||
String text, [
|
||||
Color? color,
|
||||
]) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: color),
|
||||
),
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
S.of(context)!.couldNotEstablishConnectionToTheServer,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.unknownHost:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.hostCouldNotBeResolved,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.missingClientCertificate:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.loginPageReachabilityMissingClientCertificateText,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.invalidClientCertificateConfiguration:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.incorrectOrMissingCertificatePassphrase,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.connectionTimeout:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.connectionTimedOut,
|
||||
errorColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSubmit() async {
|
||||
FocusScope.of(context).unfocus();
|
||||
setState(() {
|
||||
_isFormSubmitted = true;
|
||||
});
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final form = _formKey.currentState!.value;
|
||||
ClientCertificate? clientCert;
|
||||
@@ -162,6 +250,10 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
||||
showInfoMessage(context, error);
|
||||
} catch (error) {
|
||||
showGenericError(context, error);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isFormSubmitted = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
||||
@@ -13,18 +13,41 @@ import 'package:paperless_mobile/features/login/model/login_form_credentials.dar
|
||||
import 'package:paperless_mobile/features/login/view/add_account_page.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||
|
||||
class LoginPage extends StatelessWidget {
|
||||
const LoginPage({super.key});
|
||||
final String? initialServerUrl;
|
||||
final String? initialUsername;
|
||||
final String? initialPassword;
|
||||
final ClientCertificate? initialClientCertificate;
|
||||
|
||||
const LoginPage({
|
||||
super.key,
|
||||
this.initialServerUrl,
|
||||
this.initialUsername,
|
||||
this.initialPassword,
|
||||
this.initialClientCertificate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AddAccountPage(
|
||||
titleString: S.of(context)!.connectToPaperless,
|
||||
titleText: S.of(context)!.connectToPaperless,
|
||||
submitText: S.of(context)!.signIn,
|
||||
onSubmit: _onLogin,
|
||||
showLocalAccounts: true,
|
||||
initialServerUrl: initialServerUrl,
|
||||
initialUsername: initialUsername,
|
||||
initialPassword: initialPassword,
|
||||
initialClientCertificate: initialClientCertificate,
|
||||
bottomLeftButton: Hive.localUserAccountBox.isNotEmpty
|
||||
? TextButton(
|
||||
child: Text(S.of(context)!.logInToExistingAccount),
|
||||
onPressed: () {
|
||||
const LoginToExistingAccountRoute().go(context);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
66
lib/features/login/view/login_to_existing_account_page.dart
Normal file
66
lib/features/login/view/login_to_existing_account_page.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||
|
||||
class LoginToExistingAccountPage extends StatelessWidget {
|
||||
const LoginToExistingAccountPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: Hive.localUserAccountBox.listenable(),
|
||||
builder: (context, value, _) {
|
||||
final localAccounts = value.values;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: Text(S.of(context)!.logInToExistingAccount),
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(S.of(context)!.addAnotherAccount),
|
||||
onPressed: () {
|
||||
const LoginRoute().go(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final account = localAccounts.elementAt(index);
|
||||
return Card(
|
||||
child: UserAccountListTile(
|
||||
account: account,
|
||||
onTap: () {
|
||||
context
|
||||
.read<AuthenticationCubit>()
|
||||
.switchAccount(account.id);
|
||||
},
|
||||
trailing: IconButton(
|
||||
tooltip: S.of(context)!.remove,
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AuthenticationCubit>()
|
||||
.removeAccount(account.id);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: localAccounts.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/features/login/view/verify_identity_page.dart
Normal file
64
lib/features/login/view/verify_identity_page.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VerifyIdentityPage extends StatelessWidget {
|
||||
final String userId;
|
||||
const VerifyIdentityPage({super.key, required this.userId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
title: Text(S.of(context)!.verifyYourIdentity),
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
const LoginToExistingAccountRoute().go(context);
|
||||
},
|
||||
child: Text(S.of(context)!.goToLogin),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () =>
|
||||
context.read<AuthenticationCubit>().restoreSession(userId),
|
||||
child: Text(S.of(context)!.verifyIdentity),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate,
|
||||
textAlign: TextAlign.center,
|
||||
).paddedSymmetrically(horizontal: 16),
|
||||
const Icon(
|
||||
Icons.fingerprint,
|
||||
size: 96,
|
||||
),
|
||||
// Wrap(
|
||||
// alignment: WrapAlignment.spaceBetween,
|
||||
// runAlignment: WrapAlignment.spaceBetween,
|
||||
// runSpacing: 8,
|
||||
// spacing: 8,
|
||||
// children: [
|
||||
|
||||
// ],
|
||||
// ).padded(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -12,11 +13,16 @@ import 'obscured_input_text_form_field.dart';
|
||||
class ClientCertificateFormField extends StatefulWidget {
|
||||
static const fkClientCertificate = 'clientCertificate';
|
||||
|
||||
final String? initialPassphrase;
|
||||
final Uint8List? initialBytes;
|
||||
|
||||
final void Function(ClientCertificateFormModel? cert) onChanged;
|
||||
const ClientCertificateFormField({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
}) : super(key: key);
|
||||
this.initialPassphrase,
|
||||
this.initialBytes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ClientCertificateFormField> createState() =>
|
||||
@@ -31,7 +37,12 @@ class _ClientCertificateFormFieldState
|
||||
return FormBuilderField<ClientCertificateFormModel?>(
|
||||
key: const ValueKey('login-client-cert'),
|
||||
onChanged: widget.onChanged,
|
||||
initialValue: null,
|
||||
initialValue: widget.initialBytes != null
|
||||
? ClientCertificateFormModel(
|
||||
bytes: widget.initialBytes!,
|
||||
passphrase: widget.initialPassphrase,
|
||||
)
|
||||
: null,
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
@@ -108,8 +119,7 @@ class _ClientCertificateFormFieldState
|
||||
),
|
||||
label: S.of(context)!.passphrase,
|
||||
).padded(),
|
||||
] else
|
||||
...[]
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -122,20 +132,23 @@ class _ClientCertificateFormFieldState
|
||||
}
|
||||
|
||||
Future<void> _onSelectFile(
|
||||
FormFieldState<ClientCertificateFormModel?> field) async {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
FormFieldState<ClientCertificateFormModel?> field,
|
||||
) async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: false,
|
||||
);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
File file = File(result.files.single.path!);
|
||||
setState(() {
|
||||
_selectedFile = file;
|
||||
});
|
||||
final changedValue =
|
||||
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
||||
ClientCertificateFormModel(bytes: file.readAsBytesSync());
|
||||
field.didChange(changedValue);
|
||||
if (result == null || result.files.single.path == null) {
|
||||
return;
|
||||
}
|
||||
File file = File(result.files.single.path!);
|
||||
setState(() {
|
||||
_selectedFile = file;
|
||||
});
|
||||
final bytes = await file.readAsBytes();
|
||||
|
||||
final changedValue = field.value?.copyWith(bytes: bytes) ??
|
||||
ClientCertificateFormModel(bytes: bytes);
|
||||
field.didChange(changedValue);
|
||||
}
|
||||
|
||||
Widget _buildSelectedFileText(
|
||||
|
||||
@@ -8,11 +8,12 @@ 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;
|
||||
const ServerAddressFormField({
|
||||
Key? key,
|
||||
required this.onSubmit,
|
||||
this.initialValue,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -38,6 +39,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderField<String>(
|
||||
initialValue: widget.initialValue,
|
||||
name: ServerAddressFormField.fkServerAddress,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (value) {
|
||||
@@ -90,7 +92,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
)
|
||||
: null,
|
||||
),
|
||||
autofocus: true,
|
||||
autofocus: false,
|
||||
onSubmitted: (_) {
|
||||
onFieldSubmitted();
|
||||
_formatInput();
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class UserCredentialsFormField extends StatefulWidget {
|
||||
static const fkCredentials = 'credentials';
|
||||
|
||||
final void Function() onFieldsSubmitted;
|
||||
|
||||
final String? initialUsername;
|
||||
final String? initialPassword;
|
||||
final GlobalKey<FormBuilderState> formKey;
|
||||
const UserCredentialsFormField({
|
||||
Key? key,
|
||||
required this.onFieldsSubmitted,
|
||||
this.initialUsername,
|
||||
this.initialPassword,
|
||||
required this.formKey,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -28,6 +36,10 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderField<LoginFormCredentials?>(
|
||||
initialValue: LoginFormCredentials(
|
||||
password: widget.initialPassword,
|
||||
username: widget.initialUsername,
|
||||
),
|
||||
name: UserCredentialsFormField.fkCredentials,
|
||||
builder: (field) => AutofillGroup(
|
||||
child: Column(
|
||||
@@ -50,6 +62,17 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.usernameMustNotBeEmpty;
|
||||
}
|
||||
final serverAddress = widget.formKey.currentState!
|
||||
.getRawValue<String>(
|
||||
ServerAddressFormField.fkServerAddress);
|
||||
if (serverAddress != null) {
|
||||
final userExists = Hive.localUserAccountBox.values
|
||||
.map((e) => e.id)
|
||||
.contains('$value@$serverAddress');
|
||||
if (userExists) {
|
||||
return S.of(context)!.userAlreadyExists;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
autofillHints: const [AutofillHints.username],
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.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/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ServerConnectionPage extends StatefulWidget {
|
||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
||||
final VoidCallback onContinue;
|
||||
final String titleText;
|
||||
|
||||
const ServerConnectionPage({
|
||||
super.key,
|
||||
required this.formBuilderKey,
|
||||
required this.onContinue,
|
||||
required this.titleText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServerConnectionPage> createState() => _ServerConnectionPageState();
|
||||
}
|
||||
|
||||
class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
||||
bool _isCheckingConnection = false;
|
||||
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
toolbarHeight: kToolbarHeight - 4,
|
||||
title: Text(widget.titleText),
|
||||
bottom: PreferredSize(
|
||||
child: _isCheckingConnection
|
||||
? const LinearProgressIndicator()
|
||||
: const SizedBox(height: 4.0),
|
||||
preferredSize: const Size.fromHeight(4.0),
|
||||
),
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ServerAddressFormField(
|
||||
onSubmit: (address) {
|
||||
_updateReachability(address);
|
||||
},
|
||||
).padded(),
|
||||
ClientCertificateFormField(
|
||||
onChanged: (_) => _updateReachability(),
|
||||
).padded(),
|
||||
_buildStatusIndicator(),
|
||||
],
|
||||
).padded(),
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(S.of(context)!.testConnection),
|
||||
onPressed: _updateReachability,
|
||||
),
|
||||
FilledButton(
|
||||
child: Text(S.of(context)!.continueLabel),
|
||||
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
|
||||
? widget.onContinue
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateReachability([String? address]) async {
|
||||
setState(() {
|
||||
_isCheckingConnection = true;
|
||||
});
|
||||
final certForm = widget.formBuilderKey.currentState
|
||||
?.getRawValue(ClientCertificateFormField.fkClientCertificate)
|
||||
as ClientCertificateFormModel?;
|
||||
final status = await context
|
||||
.read<ConnectivityStatusService>()
|
||||
.isPaperlessServerReachable(
|
||||
address ??
|
||||
widget.formBuilderKey.currentState!
|
||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||
certForm != null
|
||||
? ClientCertificate(
|
||||
bytes: certForm.bytes, passphrase: certForm.passphrase)
|
||||
: null,
|
||||
);
|
||||
setState(() {
|
||||
_isCheckingConnection = false;
|
||||
_reachabilityStatus = status;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicator() {
|
||||
if (_isCheckingConnection) {
|
||||
return const ListTile();
|
||||
}
|
||||
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,
|
||||
S.of(context)!.couldNotEstablishConnectionToTheServer,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.unknownHost:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.hostCouldNotBeResolved,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.missingClientCertificate:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.loginPageReachabilityMissingClientCertificateText,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.invalidClientCertificateConfiguration:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.incorrectOrMissingCertificatePassphrase,
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.connectionTimeout:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
S.of(context)!.connectionTimedOut,
|
||||
errorColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildIconText(
|
||||
IconData icon,
|
||||
String text, [
|
||||
Color? color,
|
||||
]) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: color),
|
||||
),
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class ServerLoginPage extends StatefulWidget {
|
||||
final String submitText;
|
||||
final Future<void> Function() onSubmit;
|
||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
||||
|
||||
const ServerLoginPage({
|
||||
super.key,
|
||||
required this.onSubmit,
|
||||
required this.formBuilderKey,
|
||||
required this.submitText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServerLoginPage> createState() => _ServerLoginPageState();
|
||||
}
|
||||
|
||||
class _ServerLoginPageState extends State<ServerLoginPage> {
|
||||
bool _isLoginLoading = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serverAddress = (widget.formBuilderKey.currentState
|
||||
?.getRawValue(ServerAddressFormField.fkServerAddress)
|
||||
as String?)
|
||||
?.replaceAll(RegExp(r'https?://'), '') ??
|
||||
'';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context)!.loginPageSignInTitle),
|
||||
bottom: _isLoginLoading
|
||||
? const PreferredSize(
|
||||
preferredSize: Size.fromHeight(4.0),
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context)!.signInToServer(serverAddress) + ":",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
).padded(16),
|
||||
UserCredentialsFormField(
|
||||
onFieldsSubmitted: widget.onSubmit,
|
||||
),
|
||||
Text(
|
||||
S.of(context)!.loginRequiredPermissionsHint,
|
||||
style: Theme.of(context).textTheme.bodySmall?.apply(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
).padded(16),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: !_isLoginLoading
|
||||
? () async {
|
||||
setState(() => _isLoginLoading = true);
|
||||
try {
|
||||
await widget.onSubmit();
|
||||
} finally {
|
||||
setState(() => _isLoginLoading = false);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(S.of(context)!.signIn),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
lib/features/login/view/widgets/login_transition_page.dart
Normal file
34
lib/features/login/view/widgets/login_transition_page.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/theme.dart';
|
||||
|
||||
class LoginTransitionPage extends StatelessWidget {
|
||||
final String text;
|
||||
const LoginTransitionPage({super.key, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: buildOverlayStyle(
|
||||
Theme.of(context),
|
||||
systemNavigationBarColor: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Text(text).paddedOnly(bottom: 24),
|
||||
),
|
||||
],
|
||||
).padded(16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class NeverScrollableScrollBehavior extends ScrollBehavior {
|
||||
@override
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
||||
return const NeverScrollableScrollPhysics();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user