178 lines
5.7 KiB
Dart
178 lines
5.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../login_controller.dart';
|
|
import 'login_error_banner.dart';
|
|
|
|
/// White Card hosting the login form (heading, two text fields, error
|
|
/// banner, submit button). Submitting calls [controller.submit] and signals
|
|
/// success via [onSuccess].
|
|
class LoginCard extends StatefulWidget {
|
|
final LoginController controller;
|
|
final VoidCallback onSuccess;
|
|
|
|
const LoginCard({
|
|
required this.controller,
|
|
required this.onSuccess,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<LoginCard> createState() => _LoginCardState();
|
|
}
|
|
|
|
class _LoginCardState extends State<LoginCard> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _usernameController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
final _passwordFocus = FocusNode();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
widget.controller.addListener(_onControllerChange);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
widget.controller.removeListener(_onControllerChange);
|
|
_usernameController.dispose();
|
|
_passwordController.dispose();
|
|
_passwordFocus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onControllerChange() {
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
String? _required(String? value) =>
|
|
(value ?? '').trim().isEmpty ? 'Eingabe erforderlich' : null;
|
|
|
|
Future<void> _submit() async {
|
|
if (widget.controller.loading) return;
|
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
|
final ok = await widget.controller.submit(
|
|
_usernameController.text,
|
|
_passwordController.text,
|
|
);
|
|
if (ok && mounted) widget.onSuccess();
|
|
}
|
|
|
|
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(
|
|
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(
|
|
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(
|
|
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'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|