import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../api/client.dart'; import '../api/endpoints/auth.dart'; import '../api/error_copy.dart'; import '../api/errors.dart'; import '../theme/theme_extension.dart'; import 'auth_provider.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final _user = TextEditingController(); final _pass = TextEditingController(); bool _busy = false; String? _error; Future _submit() async { setState(() { _busy = true; _error = null; }); try { final url = await ref.read(serverUrlProvider.future); if (url == null) { if (!mounted) return; Navigator.of(context).pushReplacementNamed('/server-url'); return; } final dio = ApiClient.buildDio( baseUrl: url, tokenResolver: () async => null, ); final res = await AuthApi(dio).login( username: _user.text.trim(), password: _pass.text, ); await ref.read(authControllerProvider.notifier).setSession( token: res.token, userJson: res.rawUserJson, ); if (!mounted) return; Navigator.of(context).pushReplacementNamed('/home'); } on DioException catch (e) { final code = ApiError.fromDio(e).code; final copy = (await ErrorCopy.load()).forCode(code); if (mounted) setState(() => _error = copy); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; return Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.all(24), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 48), Text('Sign in', style: TextStyle(fontFamily: fs.display.fontFamily, fontSize: 28)), const SizedBox(height: 24), TextField(controller: _user, decoration: const InputDecoration(labelText: 'Username')), const SizedBox(height: 12), TextField(controller: _pass, obscureText: true, decoration: const InputDecoration(labelText: 'Password')), if (_error != null) Padding( padding: const EdgeInsets.only(top: 8), child: Text(_error!, style: TextStyle(color: fs.error)), ), const SizedBox(height: 16), FilledButton( onPressed: _busy ? null : _submit, child: _busy ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Sign in'), ), TextButton( onPressed: () => Navigator.of(context).pushReplacementNamed('/server-url'), child: const Text('Change server URL'), ), ]), ), ), ); } }