import 'package:flutter/material.dart'; import '../application/application.dart'; import '../domain/domain.dart'; import 'theme.dart'; final class ProfileScreen extends StatefulWidget { const ProfileScreen({required this.authUseCases, super.key}); final AuthUseCases authUseCases; @override State createState() => _ProfileScreenState(); } final class _ProfileScreenState extends State { late Future _session; @override void initState() { super.initState(); _session = widget.authUseCases.currentSession(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Profil')), body: FutureBuilder( future: _session, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } final session = snapshot.data; if (session == null || !session.isLoggedIn) { return _SignedOutProfile( onRegister: () => _openRegister(context), onLogin: () => _openLogin(context), ); } return _SignedInProfile( session: session, onLogout: () => _confirmLogout(context), onShares: () => _openReceivedShares(context), ); }, ), ); } Future _openLogin(BuildContext context) async { final connected = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => LoginScreen(authUseCases: widget.authUseCases), ), ); if (connected == true && mounted) { _reloadSession(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Compte connecté. Synchronisation en arrière-plan.'), ), ); } } Future _openRegister(BuildContext context) async { final connected = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => RegisterScreen(authUseCases: widget.authUseCases), ), ); if (connected == true && mounted) { _reloadSession(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Compte connecté. Synchronisation en arrière-plan.'), ), ); } } Future _confirmLogout(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Se déconnecter ?'), content: const Text( 'Les données restent sur cet appareil. La synchronisation et les ' 'partages seront suspendus jusqu’à une prochaine connexion.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), FilledButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Se déconnecter'), ), ], ), ); if (confirmed != true) return; await widget.authUseCases.logout(); if (!mounted) return; _reloadSession(); } void _openReceivedShares(BuildContext context) { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const ReceivedSharesScreen()), ); } void _reloadSession() { setState(() { _session = widget.authUseCases.currentSession(); }); } } final class _SignedOutProfile extends StatelessWidget { const _SignedOutProfile({required this.onRegister, required this.onLogin}); final VoidCallback onRegister; final VoidCallback onLogin; @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.all(16), children: [ CourtBlazerAccentPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Compte optionnel', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), const Text( 'GameTime fonctionne entièrement sans compte. Connecte-toi ' 'seulement si tu veux sauvegarder tes données en ligne ou ' 'partager des programmes et séances.', ), const SizedBox(height: 16), FilledButton( onPressed: onRegister, child: const Text('Créer un compte'), ), const SizedBox(height: 8), OutlinedButton( onPressed: onLogin, child: const Text('Se connecter'), ), ], ), ), const SizedBox(height: 16), CourtBlazerAccentPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Données locales', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), const Text( 'Tes exercices, programmes, séances et historiques sont ' 'enregistrés sur cet appareil.', ), ], ), ), const SizedBox(height: 16), CourtBlazerAccentPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Partages', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), const Text( 'Connecte-toi pour envoyer et recevoir des programmes ou des ' 'séances.', ), ], ), ), ], ); } } final class _SignedInProfile extends StatelessWidget { const _SignedInProfile({ required this.session, required this.onLogout, required this.onShares, }); final UserAccountSession session; final VoidCallback onLogout; final VoidCallback onShares; @override Widget build(BuildContext context) { final displayName = session.displayName?.trim(); final title = displayName == null || displayName.isEmpty ? session.email : displayName; return ListView( padding: const EdgeInsets.all(16), children: [ CourtBlazerAccentPanel( child: Row( children: [ CircleAvatar(radius: 28, child: Text(_profileInitials(title))), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.titleLarge), if (title != session.email) ...[ const SizedBox(height: 4), Text(session.email), ], ], ), ), ], ), ), const SizedBox(height: 16), OutlinedButton.icon( onPressed: onLogout, icon: const Icon(Icons.logout), label: const Text('Se déconnecter'), ), const SizedBox(height: 16), CourtBlazerAccentPanel( child: Material( type: MaterialType.transparency, child: ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.cloud_outlined), title: const Text('Synchronisation'), subtitle: const Text('Synchronisation : à venir'), ), ), ), const SizedBox(height: 12), CourtBlazerAccentPanel( child: Material( type: MaterialType.transparency, child: ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.inbox_outlined), title: const Text('Partages reçus'), subtitle: const Text( "Programmes et séances reçus d'autres comptes", ), trailing: const Icon(Icons.chevron_right), onTap: onShares, ), ), ), ], ); } } final class LoginScreen extends StatefulWidget { const LoginScreen({required this.authUseCases, super.key}); final AuthUseCases authUseCases; @override State createState() => _LoginScreenState(); } final class _LoginScreenState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); String? _error; var _submitting = false; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Se connecter')), body: _AuthFormScaffold( child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: _emailValidator, ), const SizedBox(height: 12), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Mot de passe'), obscureText: true, validator: _passwordValidator, ), if (_error != null) ...[ const SizedBox(height: 12), _InlineAuthError(message: _error!), ], const SizedBox(height: 16), FilledButton( onPressed: _submitting ? null : _submit, child: Text(_submitting ? 'Connexion...' : 'Se connecter'), ), const SizedBox(height: 8), OutlinedButton( onPressed: _submitting ? null : () { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => RegisterScreen( authUseCases: widget.authUseCases, ), ), ); }, child: const Text('Créer un compte'), ), const SizedBox(height: 16), Text( 'Tu peux continuer à utiliser GameTime sans compte.', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ); } Future _submit() async { if (!_formKey.currentState!.validate()) return; setState(() { _error = null; _submitting = true; }); try { await widget.authUseCases.login( email: _emailController.text, password: _passwordController.text, ); if (!mounted) return; Navigator.of(context).pop(true); } on RemoteAuthException catch (error) { if (!mounted) return; setState(() { _error = _loginErrorMessage(error.failure); _submitting = false; }); } on DomainException { if (!mounted) return; setState(() { _error = 'Vérifie les informations saisies.'; _submitting = false; }); } } } final class RegisterScreen extends StatefulWidget { const RegisterScreen({required this.authUseCases, super.key}); final AuthUseCases authUseCases; @override State createState() => _RegisterScreenState(); } final class _RegisterScreenState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); final _displayNameController = TextEditingController(); String? _error; var _submitting = false; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); _displayNameController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Créer un compte')), body: _AuthFormScaffold( child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: _emailValidator, ), const SizedBox(height: 12), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Mot de passe'), obscureText: true, validator: _newPasswordValidator, ), const SizedBox(height: 12), TextFormField( controller: _confirmPasswordController, decoration: const InputDecoration( labelText: 'Confirmer le mot de passe', ), obscureText: true, validator: (value) { if (value != _passwordController.text) { return 'Les mots de passe ne correspondent pas.'; } return null; }, ), const SizedBox(height: 12), TextFormField( controller: _displayNameController, decoration: const InputDecoration( labelText: 'Pseudo (optionnel)', ), ), if (_error != null) ...[ const SizedBox(height: 12), _InlineAuthError(message: _error!), ], const SizedBox(height: 16), FilledButton( onPressed: _submitting ? null : _submit, child: Text(_submitting ? 'Création...' : 'Créer le compte'), ), const SizedBox(height: 8), OutlinedButton( onPressed: _submitting ? null : () { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => LoginScreen(authUseCases: widget.authUseCases), ), ); }, child: const Text('Déjà un compte ? Se connecter'), ), const SizedBox(height: 16), Text( 'Le compte sert à synchroniser tes données et partager tes ' "contenus. L'app reste utilisable sans compte.", style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ); } Future _submit() async { if (!_formKey.currentState!.validate()) return; setState(() { _error = null; _submitting = true; }); final displayName = _displayNameController.text.trim(); try { await widget.authUseCases.register( email: _emailController.text, password: _passwordController.text, displayName: displayName.isEmpty ? null : displayName, ); if (!mounted) return; Navigator.of(context).pop(true); } on RemoteAuthException catch (error) { if (!mounted) return; setState(() { _error = _registerErrorMessage(error.failure); _submitting = false; }); } on DomainException { if (!mounted) return; setState(() { _error = 'Vérifie les informations saisies.'; _submitting = false; }); } } } final class ReceivedSharesScreen extends StatelessWidget { const ReceivedSharesScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Partages reçus')), body: const Center( child: Padding( padding: EdgeInsets.all(24), child: Text( "Les programmes et séances qu'on t'envoie apparaîtront ici.", textAlign: TextAlign.center, ), ), ), ); } } final class _AuthFormScaffold extends StatelessWidget { const _AuthFormScaffold({required this.child}); final Widget child; @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.all(16), children: [CourtBlazerAccentPanel(child: child)], ); } } final class _InlineAuthError extends StatelessWidget { const _InlineAuthError({required this.message}); final String message; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Text( message, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, ), ); } } String? _emailValidator(String? value) { final email = value?.trim() ?? ''; if (!RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(email)) { return 'Saisis une adresse email valide.'; } return null; } String? _passwordValidator(String? value) { final password = value ?? ''; if (password.isEmpty) { return 'Saisis ton mot de passe.'; } if (password.length < 8) { return 'Saisis un mot de passe d’au moins 8 caractères.'; } return null; } String? _newPasswordValidator(String? value) { if ((value ?? '').length < 8) { return 'Saisis un mot de passe d’au moins 8 caractères.'; } return null; } String _loginErrorMessage(RemoteAuthFailure failure) { return switch (failure) { RemoteAuthFailure.invalidCredentials => 'Email ou mot de passe incorrect.', RemoteAuthFailure.network => 'Connexion impossible pour le moment. Réessaie plus tard.', _ => 'Connexion impossible pour le moment. Réessaie plus tard.', }; } String _registerErrorMessage(RemoteAuthFailure failure) { return switch (failure) { RemoteAuthFailure.emailAlreadyUsed => 'Un compte existe déjà avec cet email.', RemoteAuthFailure.network => 'Création impossible pour le moment. Réessaie plus tard.', _ => 'Création impossible pour le moment. Réessaie plus tard.', }; } String _profileInitials(String value) { final trimmed = value.trim(); if (trimmed.isEmpty) return '?'; final words = trimmed.split(RegExp(r'\s+')); if (words.length == 1) { return words.first.substring(0, 1).toUpperCase(); } return '${words.first.substring(0, 1)}${words.last.substring(0, 1)}' .toUpperCase(); }