// Version: 1.1.0 | Created: 2026-04-01 | Updated: 2026-04-02 // Login screen. Username + password only. No registration link. // Respects system theme preference. import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/config/app_config.dart'; import 'login_controller.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final _formKey = GlobalKey(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); bool _passwordVisible = false; @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); super.dispose(); } Future _submit() async { if (!(_formKey.currentState?.validate() ?? false)) return; final failure = await ref .read(loginControllerProvider.notifier) .login( username: _usernameController.text.trim(), password: _passwordController.text, ); if (failure != null && mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(failure), backgroundColor: Theme.of(context).colorScheme.error, ), ); } } @override Widget build(BuildContext context) { final isLoading = ref.watch(loginControllerProvider); final theme = Theme.of(context); return Scaffold( body: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _LogoSection(theme: theme), const SizedBox(height: 48), _UsernameField(controller: _usernameController), const SizedBox(height: 16), _PasswordField( controller: _passwordController, isVisible: _passwordVisible, onToggleVisibility: () { setState(() => _passwordVisible = !_passwordVisible); }, onSubmit: _submit, ), const SizedBox(height: 32), _SignInButton(isLoading: isLoading, onPressed: _submit), const SizedBox(height: 16), _ServerLabel(theme: theme), ], ), ), ), ), ), ), ); } } class _LogoSection extends StatelessWidget { const _LogoSection({required this.theme}); final ThemeData theme; @override Widget build(BuildContext context) { return Column( children: [ Image.asset( 'assets/images/logo.png', width: 96, height: 96, errorBuilder: (_, __, ___) => CircleAvatar( radius: 48, backgroundColor: theme.colorScheme.primary, child: const Icon(Icons.chat_bubble, size: 48, color: Colors.white), ), ), const SizedBox(height: 20), Text( 'M8Chat', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), const SizedBox(height: 8), Text( 'Sign in to continue', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withAlpha(153), ), ), ], ); } } class _UsernameField extends StatelessWidget { const _UsernameField({required this.controller}); final TextEditingController controller; @override Widget build(BuildContext context) { return TextFormField( controller: controller, decoration: const InputDecoration( labelText: 'Username', hintText: 'Enter your username', prefixIcon: Icon(Icons.person_outline), ), textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Please enter your username.'; } return null; }, ); } } class _PasswordField extends StatelessWidget { const _PasswordField({ required this.controller, required this.isVisible, required this.onToggleVisibility, required this.onSubmit, }); final TextEditingController controller; final bool isVisible; final VoidCallback onToggleVisibility; final VoidCallback onSubmit; @override Widget build(BuildContext context) { return TextFormField( controller: controller, decoration: InputDecoration( labelText: 'Password', hintText: 'Enter your password', prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon(isVisible ? Icons.visibility_off : Icons.visibility), onPressed: onToggleVisibility, tooltip: isVisible ? 'Hide password' : 'Show password', ), ), obscureText: !isVisible, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => onSubmit(), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your password.'; } return null; }, ); } } class _SignInButton extends StatelessWidget { const _SignInButton({required this.isLoading, required this.onPressed}); final bool isLoading; final VoidCallback onPressed; @override Widget build(BuildContext context) { return ElevatedButton( onPressed: isLoading ? null : onPressed, child: isLoading ? const SizedBox( height: 22, width: 22, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : const Text('Sign In'), ); } } class _ServerLabel extends StatelessWidget { const _ServerLabel({required this.theme}); final ThemeData theme; @override Widget build(BuildContext context) { return Text( AppConfig.matrixServerName, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withAlpha(102), ), ); } }