feat: bugfixes, finished go_router migration, implemented better visibility of states

This commit is contained in:
Anton Stubenbord
2023-10-06 01:17:08 +02:00
parent ad23df4f89
commit a2c5ced3b7
102 changed files with 1512 additions and 3090 deletions

View File

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

View File

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

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

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

View File

@@ -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(

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1,8 +0,0 @@
import 'package:flutter/widgets.dart';
class NeverScrollableScrollBehavior extends ScrollBehavior {
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
return const NeverScrollableScrollPhysics();
}
}