reworked login screen texts and extracted shared login form parts

This commit is contained in:
2026-09-27 14:40:04 +02:00
parent 1fd6264d10
commit 79b33ab248
4 changed files with 163 additions and 132 deletions
+1 -2
View File
@@ -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(
+1 -19
View File
@@ -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});
+15 -74
View File
@@ -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),
),
);
}