From 79b33ab248a36b76f9ea6a3b13cec7a72564c4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 27 Sep 2026 14:40:04 +0200 Subject: [PATCH] reworked login screen texts and extracted shared login form parts --- lib/view/login/login.dart | 3 +- lib/view/login/widgets/login_branding.dart | 20 +-- lib/view/login/widgets/login_card.dart | 163 ++++++------------- lib/view/login/widgets/login_form_parts.dart | 109 +++++++++++++ 4 files changed, 163 insertions(+), 132 deletions(-) create mode 100644 lib/view/login/widgets/login_form_parts.dart diff --git a/lib/view/login/login.dart b/lib/view/login/login.dart index d34a9f4..8b0262e 100644 --- a/lib/view/login/login.dart +++ b/lib/view/login/login.dart @@ -93,8 +93,7 @@ class _LoginState extends State with SingleTickerProviderStateMixin { controller: _controller, onSuccess: _onLoginSuccess, ), - const SizedBox(height: 18), - const LoginDisclaimer(), + const SizedBox(height: 12), ], ), const Column( diff --git a/lib/view/login/widgets/login_branding.dart b/lib/view/login/widgets/login_branding.dart index d25167c..9d6bf2a 100644 --- a/lib/view/login/widgets/login_branding.dart +++ b/lib/view/login/widgets/login_branding.dart @@ -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}); diff --git a/lib/view/login/widgets/login_card.dart b/lib/view/login/widgets/login_card.dart index a259d98..a38c152 100644 --- a/lib/view/login/widgets/login_card.dart +++ b/lib/view/login/widgets/login_card.dart @@ -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,122 +76,62 @@ class _LoginCardState extends State { } } - 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( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - 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, - enabled: !loading, - validator: _required, - autocorrect: false, - textInputAction: TextInputAction.next, - onFieldSubmitted: (_) => _passwordFocus.requestFocus(), - decoration: _decoration( - theme, - 'Nutzername', - Icons.person_outline, - ), - ), - const SizedBox(height: 12), - TextFormField( - key: const Key('login-password-field'), - controller: _passwordController, - focusNode: _passwordFocus, - enabled: !loading, - validator: _required, - obscureText: true, - obscuringCharacter: '•', - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) => _submit(), - decoration: _decoration(theme, 'Passwort', Icons.lock_outline), - ), - LoginErrorBanner( - message: widget.controller.errorMessage, - details: widget.controller.errorDetails, - ), - const SizedBox(height: 20), - SizedBox( - height: 50, - child: FilledButton( - 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'), - ), - ), - ], + return Form( + key: _formKey, + child: LoginCardFrame( + title: 'Anmelden', + hint: 'Melde dich mit deinen Marianum-Zugangsdaten an.', + children: [ + TextFormField( + key: const Key('login-username-field'), + controller: _usernameController, + enabled: !loading, + validator: _required, + autocorrect: false, + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) => _passwordFocus.requestFocus(), + decoration: loginInputDecoration( + theme, + 'Nutzername', + Icons.person_outline, + ), ), - ), + const SizedBox(height: 12), + TextFormField( + key: const Key('login-password-field'), + controller: _passwordController, + focusNode: _passwordFocus, + enabled: !loading, + validator: _required, + obscureText: true, + obscuringCharacter: '•', + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) => _submit(), + decoration: loginInputDecoration( + theme, + 'Passwort', + Icons.lock_outline, + ), + ), + LoginErrorBanner( + message: widget.controller.errorMessage, + details: widget.controller.errorDetails, + ), + const SizedBox(height: 20), + LoginSubmitButton( + key: const Key('login-submit-button'), + label: 'Anmelden', + loading: loading, + onPressed: _submit, + ), + ], ), ); } diff --git a/lib/view/login/widgets/login_form_parts.dart b/lib/view/login/widgets/login_form_parts.dart new file mode 100644 index 0000000..6f6d6b2 --- /dev/null +++ b/lib/view/login/widgets/login_form_parts.dart @@ -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 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), + ), + ); +}