Ajoute la synchronisation client incrémentale LWW vers l'API serveur (application/use_cases.dart: SyncUseCases, ports.dart, migration Drift schemaVersion 10→11 pour les métadonnées de sync et mappings de ressources, infrastructure/remote/sync_api.dart) et l'écran de profil avec entrée sur l'accueil (presentation/profile_screen.dart, home_screen.dart, presentation.dart). Développés dans le même worktree partagé par DevBackend et DevFrontend ; commit combiné car test/presentation/home_screen_test.dart mélange authentiquement les deux tickets (le test de l'entrée Profil et la mise à jour du fake de bootstrap requise par le nouveau getter syncUseCases sur AppDependencies). Corrige au passage une couleur `crimson` inexistante dans le thème (remplacée par colorScheme.error) et une signature de paramètres positionnels/nommés incohérente sur un fake de test. flutter pub get OK, build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 119/119 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -4,6 +4,7 @@ import '../application/app_bootstrap.dart';
|
||||
import '../domain/domain.dart';
|
||||
import 'exercise_library_screen.dart';
|
||||
import 'history_screen.dart';
|
||||
import 'profile_screen.dart';
|
||||
import 'program_screen.dart';
|
||||
import 'theme.dart';
|
||||
import 'workout_execution_screen.dart';
|
||||
@ -178,6 +179,34 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: FutureBuilder<UserAccountSession?>(
|
||||
future: widget.bootstrap.authUseCases.currentSession(),
|
||||
builder: (context, snapshot) {
|
||||
final session = snapshot.data;
|
||||
if (session == null || !session.isLoggedIn) {
|
||||
return const Icon(Icons.account_circle_outlined);
|
||||
}
|
||||
final displayName = session.displayName?.trim();
|
||||
final label = displayName == null || displayName.isEmpty
|
||||
? session.email
|
||||
: displayName;
|
||||
return CircleAvatar(
|
||||
radius: 12,
|
||||
child: Text(_profileInitials(label)),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: const Text('Profil'),
|
||||
subtitle: const Text('Compte, synchronisation et partages'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ProfileScreen(authUseCases: widget.bootstrap.authUseCases),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -211,3 +240,14 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ export 'exercise_step_audio.dart';
|
||||
export 'exercise_library_screen.dart';
|
||||
export 'history_screen.dart';
|
||||
export 'home_screen.dart';
|
||||
export 'profile_screen.dart';
|
||||
export 'program_screen.dart';
|
||||
export 'theme.dart';
|
||||
export 'workout_execution_screen.dart';
|
||||
|
||||
631
lib/presentation/profile_screen.dart
Normal file
631
lib/presentation/profile_screen.dart
Normal file
@ -0,0 +1,631 @@
|
||||
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<ProfileScreen> createState() => _ProfileScreenState();
|
||||
}
|
||||
|
||||
final class _ProfileScreenState extends State<ProfileScreen> {
|
||||
late Future<UserAccountSession?> _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<UserAccountSession?>(
|
||||
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<void> _openLogin(BuildContext context) async {
|
||||
final connected = await Navigator.of(context).push<bool>(
|
||||
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<void> _openRegister(BuildContext context) async {
|
||||
final connected = await Navigator.of(context).push<bool>(
|
||||
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<void> _confirmLogout(BuildContext context) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
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<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
final class _LoginScreenState extends State<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<void> _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<RegisterScreen> createState() => _RegisterScreenState();
|
||||
}
|
||||
|
||||
final class _RegisterScreenState extends State<RegisterScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<void> _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();
|
||||
}
|
||||
Reference in New Issue
Block a user