reworked login screen texts and extracted shared login form parts
This commit is contained in:
@@ -93,8 +93,7 @@ class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
|
|||||||
controller: _controller,
|
controller: _controller,
|
||||||
onSuccess: _onLoginSuccess,
|
onSuccess: _onLoginSuccess,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 12),
|
||||||
const LoginDisclaimer(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Column(
|
const Column(
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class LoginHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'Stundenplan, Talk & Dateien an einem Ort.',
|
'Stundenplan, Talk, Dateien und mehr - alles an einem Ort für deinen Schulalltag am Marianum Fulda.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.85),
|
color: Colors.white.withValues(alpha: 0.85),
|
||||||
@@ -41,24 +41,6 @@ class LoginHeader extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoginDisclaimer extends StatelessWidget {
|
|
||||||
const LoginDisclaimer({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: Text(
|
|
||||||
'Alles für deinen Schulalltag am Marianum Fulda.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white.withValues(alpha: 0.75),
|
|
||||||
fontSize: 11,
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class LoginFooter extends StatelessWidget {
|
class LoginFooter extends StatelessWidget {
|
||||||
const LoginFooter({super.key});
|
const LoginFooter({super.key});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import '../../../routing/app_routes.dart';
|
import '../../../routing/app_routes.dart';
|
||||||
import '../login_controller.dart';
|
import '../login_controller.dart';
|
||||||
import 'login_error_banner.dart';
|
import 'login_error_banner.dart';
|
||||||
|
import 'login_form_parts.dart';
|
||||||
|
|
||||||
/// White Card hosting the login form (heading, two text fields, error
|
/// White Card hosting the login form (heading, two text fields, error
|
||||||
/// banner, submit button). Submitting calls [controller.submit] and signals
|
/// banner, submit button). Submitting calls [controller.submit] and signals
|
||||||
@@ -75,58 +76,16 @@ class _LoginCardState extends State<LoginCard> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _decoration(ThemeData theme, String label, IconData icon) =>
|
|
||||||
InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
prefixIcon: Icon(icon),
|
|
||||||
filled: true,
|
|
||||||
fillColor: theme.colorScheme.surfaceContainerHighest.withValues(
|
|
||||||
alpha: 0.4,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: theme.colorScheme.primary, width: 1.5),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final loading = widget.controller.loading;
|
final loading = widget.controller.loading;
|
||||||
return Card(
|
return Form(
|
||||||
elevation: 8,
|
|
||||||
shadowColor: Colors.black.withValues(alpha: 0.35),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
||||||
color: theme.colorScheme.surface,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 20),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: LoginCardFrame(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
title: 'Anmelden',
|
||||||
|
hint: 'Melde dich mit deinen Marianum-Zugangsdaten an.',
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'Anmelden',
|
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
'Melde dich mit deinen Marianum-Zugangsdaten an.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
key: const Key('login-username-field'),
|
key: const Key('login-username-field'),
|
||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
@@ -135,7 +94,7 @@ class _LoginCardState extends State<LoginCard> {
|
|||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onFieldSubmitted: (_) => _passwordFocus.requestFocus(),
|
onFieldSubmitted: (_) => _passwordFocus.requestFocus(),
|
||||||
decoration: _decoration(
|
decoration: loginInputDecoration(
|
||||||
theme,
|
theme,
|
||||||
'Nutzername',
|
'Nutzername',
|
||||||
Icons.person_outline,
|
Icons.person_outline,
|
||||||
@@ -155,43 +114,25 @@ class _LoginCardState extends State<LoginCard> {
|
|||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onFieldSubmitted: (_) => _submit(),
|
onFieldSubmitted: (_) => _submit(),
|
||||||
decoration: _decoration(theme, 'Passwort', Icons.lock_outline),
|
decoration: loginInputDecoration(
|
||||||
|
theme,
|
||||||
|
'Passwort',
|
||||||
|
Icons.lock_outline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
LoginErrorBanner(
|
LoginErrorBanner(
|
||||||
message: widget.controller.errorMessage,
|
message: widget.controller.errorMessage,
|
||||||
details: widget.controller.errorDetails,
|
details: widget.controller.errorDetails,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
SizedBox(
|
LoginSubmitButton(
|
||||||
height: 50,
|
|
||||||
child: FilledButton(
|
|
||||||
key: const Key('login-submit-button'),
|
key: const Key('login-submit-button'),
|
||||||
onPressed: loading ? null : _submit,
|
label: 'Anmelden',
|
||||||
style: FilledButton.styleFrom(
|
loading: loading,
|
||||||
shape: RoundedRectangleBorder(
|
onPressed: _submit,
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
textStyle: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: loading
|
|
||||||
? const SizedBox(
|
|
||||||
height: 22,
|
|
||||||
width: 22,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2.5,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('Anmelden'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Filled, borderless text field look shared by the login cards.
|
||||||
|
InputDecoration loginInputDecoration(
|
||||||
|
ThemeData theme,
|
||||||
|
String label,
|
||||||
|
IconData icon,
|
||||||
|
) => InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
prefixIcon: Icon(icon),
|
||||||
|
filled: true,
|
||||||
|
fillColor: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: theme.colorScheme.primary, width: 1.5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Card frame with heading and hint line shared by the login cards.
|
||||||
|
class LoginCardFrame extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String hint;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
const LoginCardFrame({
|
||||||
|
required this.title,
|
||||||
|
required this.hint,
|
||||||
|
required this.children,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Card(
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: Colors.black.withValues(alpha: 0.35),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 24, 24, 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
hint,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
...children,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full-width primary button that swaps its label for a spinner while busy.
|
||||||
|
class LoginSubmitButton extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool loading;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
const LoginSubmitButton({
|
||||||
|
required this.label,
|
||||||
|
required this.loading,
|
||||||
|
required this.onPressed,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: loading ? null : onPressed,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
child: loading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 22,
|
||||||
|
width: 22,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(label),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user