feat(online): sync client incrémentale LWW et profil/écrans auth (tickets #65, #67)

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:
2026-07-19 21:41:18 +02:00
parent b643988986
commit c0dbce275b
16 changed files with 3913 additions and 3 deletions

View File

@ -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();
}