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,
onSuccess: _onLoginSuccess,
),
const SizedBox(height: 18),
const LoginDisclaimer(),
const SizedBox(height: 12),
],
),
const Column(
+1 -19
View File
@@ -29,7 +29,7 @@ class LoginHeader extends StatelessWidget {
),
const SizedBox(height: 6),
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,
style: TextStyle(
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 {
const LoginFooter({super.key});
+15 -74
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../../../routing/app_routes.dart';
import '../login_controller.dart';
import 'login_error_banner.dart';
import 'login_form_parts.dart';
/// White Card hosting the login form (heading, two text fields, error
/// 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
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loading = widget.controller.loading;
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: Form(
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
child: LoginCardFrame(
title: 'Anmelden',
hint: 'Melde dich mit deinen Marianum-Zugangsdaten an.',
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(
key: const Key('login-username-field'),
controller: _usernameController,
@@ -135,7 +94,7 @@ class _LoginCardState extends State<LoginCard> {
autocorrect: false,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => _passwordFocus.requestFocus(),
decoration: _decoration(
decoration: loginInputDecoration(
theme,
'Nutzername',
Icons.person_outline,
@@ -155,43 +114,25 @@ class _LoginCardState extends State<LoginCard> {
keyboardType: TextInputType.visiblePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
decoration: _decoration(theme, 'Passwort', Icons.lock_outline),
decoration: loginInputDecoration(
theme,
'Passwort',
Icons.lock_outline,
),
),
LoginErrorBanner(
message: widget.controller.errorMessage,
details: widget.controller.errorDetails,
),
const SizedBox(height: 20),
SizedBox(
height: 50,
child: FilledButton(
LoginSubmitButton(
key: const Key('login-submit-button'),
onPressed: loading ? null : _submit,
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,
),
)
: const Text('Anmelden'),
),
label: 'Anmelden',
loading: loading,
onPressed: _submit,
),
],
),
),
),
);
}
}
@@ -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),
),
);
}