117 lines
4.1 KiB
Dart
117 lines
4.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../core/api/api_error_mapper.dart';
|
|
import '../../../core/l10n/l10n.dart';
|
|
import '../data/auth_providers.dart';
|
|
|
|
class LoginScreen extends ConsumerStatefulWidget {
|
|
const LoginScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<LoginScreen> createState() => _LoginScreenState();
|
|
}
|
|
|
|
class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _usernameCtrl = TextEditingController();
|
|
final _passwordCtrl = TextEditingController();
|
|
final _passwordFocus = FocusNode();
|
|
|
|
@override
|
|
void dispose() {
|
|
_usernameCtrl.dispose();
|
|
_passwordCtrl.dispose();
|
|
_passwordFocus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
if (!(_formKey.currentState?.validate() ?? false)) {
|
|
return;
|
|
}
|
|
await ref.read(authStateProvider.notifier).login(
|
|
_usernameCtrl.text.trim(),
|
|
_passwordCtrl.text,
|
|
);
|
|
// Router redirect handles navigation when authStateProvider updates.
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final authState = ref.watch(authStateProvider);
|
|
final isLoading = authState is AsyncLoading;
|
|
final l10n = context.l10n;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(l10n.loginTitle)),
|
|
body: Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 400),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
TextFormField(
|
|
controller: _usernameCtrl,
|
|
decoration: InputDecoration(labelText: l10n.usernameLabel),
|
|
textInputAction: TextInputAction.next,
|
|
autofocus: true,
|
|
enabled: !isLoading,
|
|
onFieldSubmitted: (_) =>
|
|
FocusScope.of(context).requestFocus(_passwordFocus),
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return l10n.usernameRequired;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _passwordCtrl,
|
|
focusNode: _passwordFocus,
|
|
decoration: InputDecoration(labelText: l10n.passwordLabel),
|
|
obscureText: true,
|
|
textInputAction: TextInputAction.done,
|
|
enabled: !isLoading,
|
|
onFieldSubmitted: (_) => _submit(),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return l10n.passwordRequired;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 28),
|
|
if (isLoading)
|
|
const Center(child: CircularProgressIndicator())
|
|
else
|
|
FilledButton(
|
|
onPressed: _submit,
|
|
child: Text(l10n.loginAction),
|
|
),
|
|
if (authState is AsyncError)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 16),
|
|
child: Text(
|
|
mapErrorToUserMessage(authState.error!, context),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.error),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|