Ajoute l'écran de partage sortant (presentation/share_screen.dart) et la boîte de réception des partages (presentation/share_inbox_screen.dart), avec les points d'entrée sur profile_screen.dart, home_screen.dart, program_screen.dart et workout_template_screen.dart. flutter analyze propre (mêmes infos préexistantes), 132/132 tests verts, build APK debug validé. Dernier ticket de développement du chantier "Ajouter les features serveur au client" (#63) — il ne reste que la QA finale (#70). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
793 lines
24 KiB
Dart
793 lines
24 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../application/application.dart';
|
||
import '../domain/domain.dart';
|
||
import 'share_inbox_screen.dart';
|
||
import 'theme.dart';
|
||
|
||
final class ProfileScreen extends StatefulWidget {
|
||
const ProfileScreen({
|
||
required this.authUseCases,
|
||
required this.syncUseCases,
|
||
required this.shareUseCases,
|
||
super.key,
|
||
});
|
||
|
||
final AuthUseCases authUseCases;
|
||
final SyncUseCases syncUseCases;
|
||
final ShareUseCases shareUseCases;
|
||
|
||
@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,
|
||
syncUseCases: widget.syncUseCases,
|
||
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,
|
||
syncUseCases: widget.syncUseCases,
|
||
),
|
||
),
|
||
);
|
||
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,
|
||
syncUseCases: widget.syncUseCases,
|
||
),
|
||
),
|
||
);
|
||
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) =>
|
||
ShareInboxScreen(shareUseCases: widget.shareUseCases),
|
||
),
|
||
);
|
||
}
|
||
|
||
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.syncUseCases,
|
||
required this.onLogout,
|
||
required this.onShares,
|
||
});
|
||
|
||
final UserAccountSession session;
|
||
final SyncUseCases syncUseCases;
|
||
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),
|
||
SyncStatusPanel(syncUseCases: syncUseCases),
|
||
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 SyncStatusPanel extends StatefulWidget {
|
||
const SyncStatusPanel({required this.syncUseCases, super.key});
|
||
|
||
final SyncUseCases syncUseCases;
|
||
|
||
@override
|
||
State<SyncStatusPanel> createState() => _SyncStatusPanelState();
|
||
}
|
||
|
||
final class _SyncStatusPanelState extends State<SyncStatusPanel> {
|
||
late Future<SyncMetadataSnapshot> _status;
|
||
var _syncing = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_status = widget.syncUseCases.currentStatus();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return CourtBlazerAccentPanel(
|
||
child: FutureBuilder<SyncMetadataSnapshot>(
|
||
future: _status,
|
||
builder: (context, snapshot) {
|
||
final status = snapshot.data;
|
||
final view = _syncStatusView(status, _syncing);
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 2),
|
||
child: _syncing
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: Icon(view.icon),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Synchronisation',
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(view.label),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
OutlinedButton.icon(
|
||
onPressed: _syncing ? null : _syncNow,
|
||
icon: const Icon(Icons.sync),
|
||
label: const Text('Synchroniser maintenant'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _syncNow() async {
|
||
setState(() {
|
||
_syncing = true;
|
||
});
|
||
await widget.syncUseCases.synchronize(manual: true);
|
||
if (!mounted) return;
|
||
setState(() {
|
||
_syncing = false;
|
||
_status = widget.syncUseCases.currentStatus();
|
||
});
|
||
}
|
||
}
|
||
|
||
final class _SyncStatusView {
|
||
const _SyncStatusView({required this.label, required this.icon});
|
||
|
||
final String label;
|
||
final IconData icon;
|
||
}
|
||
|
||
_SyncStatusView _syncStatusView(SyncMetadataSnapshot? metadata, bool syncing) {
|
||
if (syncing || metadata?.status == OnlineSyncStatus.syncing) {
|
||
return const _SyncStatusView(
|
||
label: 'Synchronisation en cours...',
|
||
icon: Icons.sync,
|
||
);
|
||
}
|
||
if (metadata == null) {
|
||
return const _SyncStatusView(
|
||
label: 'Synchronisation en attente',
|
||
icon: Icons.cloud_outlined,
|
||
);
|
||
}
|
||
final lastSuccess = metadata.lastSuccessfulSyncAt;
|
||
if (metadata.status == OnlineSyncStatus.success && lastSuccess != null) {
|
||
return _SyncStatusView(
|
||
label: 'Dernière synchro : ${_relativeSyncTime(lastSuccess)}',
|
||
icon: Icons.cloud_done_outlined,
|
||
);
|
||
}
|
||
if (metadata.status == OnlineSyncStatus.failure) {
|
||
if (lastSuccess != null) {
|
||
return _SyncStatusView(
|
||
label:
|
||
'Dernière synchronisation : ${_relativeSyncTime(lastSuccess)}. '
|
||
'Nouvelle tentative automatique.',
|
||
icon: Icons.schedule,
|
||
);
|
||
}
|
||
return const _SyncStatusView(
|
||
label: 'Synchronisation en attente',
|
||
icon: Icons.schedule,
|
||
);
|
||
}
|
||
return const _SyncStatusView(
|
||
label: 'Synchronisation en attente',
|
||
icon: Icons.cloud_outlined,
|
||
);
|
||
}
|
||
|
||
final class LoginScreen extends StatefulWidget {
|
||
const LoginScreen({
|
||
required this.authUseCases,
|
||
required this.syncUseCases,
|
||
super.key,
|
||
});
|
||
|
||
final AuthUseCases authUseCases;
|
||
final SyncUseCases syncUseCases;
|
||
|
||
@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,
|
||
syncUseCases: widget.syncUseCases,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
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,
|
||
required this.syncUseCases,
|
||
super.key,
|
||
});
|
||
|
||
final AuthUseCases authUseCases;
|
||
final SyncUseCases syncUseCases;
|
||
|
||
@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,
|
||
syncUseCases: widget.syncUseCases,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
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 _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 _relativeSyncTime(DateTime value) {
|
||
final localValue = value.toLocal();
|
||
final difference = DateTime.now().difference(localValue);
|
||
if (difference.inMinutes < 1) {
|
||
return 'à l’instant';
|
||
}
|
||
if (difference.inHours < 1) {
|
||
return 'il y a ${difference.inMinutes} min';
|
||
}
|
||
if (difference.inDays < 1) {
|
||
return 'il y a ${difference.inHours} h';
|
||
}
|
||
if (difference.inDays == 1) {
|
||
return 'hier à ${_formatHourMinute(localValue)}';
|
||
}
|
||
return 'le ${localValue.day.toString().padLeft(2, '0')}/'
|
||
'${localValue.month.toString().padLeft(2, '0')} '
|
||
'à ${_formatHourMinute(localValue)}';
|
||
}
|
||
|
||
String _formatHourMinute(DateTime value) {
|
||
final hour = value.hour.toString().padLeft(2, '0');
|
||
final minute = value.minute.toString().padLeft(2, '0');
|
||
return '$hour:$minute';
|
||
}
|
||
|
||
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();
|
||
}
|