Files
GameTime/lib/presentation/home_screen.dart
Blomios 3b7d904953 feat(statistiques): implémentation frontend statistiques de progression (ticket #82)
Lots F1+F2 : écran progression_screen.dart, intégration home/history/navigation, tests associés (193 tests suite complète, 4 échecs préexistants sans rapport confirmés).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 08:58:16 +02:00

298 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import '../application/app_bootstrap.dart';
import '../domain/domain.dart';
import 'exercise_library_screen.dart';
import 'history_screen.dart';
import 'profile_screen.dart';
import 'progression_screen.dart';
import 'program_screen.dart';
import 'theme.dart';
import 'workout_execution_screen.dart';
import 'workout_template_screen.dart';
final homeRouteObserver = RouteObserver<ModalRoute<void>>();
final class HomeScreen extends StatefulWidget {
const HomeScreen({
required this.bootstrap,
this.themeMode = ThemeMode.system,
this.onThemeModeChanged,
super.key,
});
final AppDependencies bootstrap;
final ThemeMode themeMode;
final ValueChanged<ThemeMode>? onThemeModeChanged;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
final class _HomeScreenState extends State<HomeScreen> with RouteAware {
late Future<ActiveWorkoutSession?> _openSession;
ModalRoute<void>? _route;
@override
void initState() {
super.initState();
_openSession = _findOpenSession();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of<void>(context);
if (route == null || identical(route, _route)) {
return;
}
final previousRoute = _route;
if (previousRoute != null) {
homeRouteObserver.unsubscribe(this);
}
_route = route;
homeRouteObserver.subscribe(this, route);
}
@override
void dispose() {
homeRouteObserver.unsubscribe(this);
super.dispose();
}
@override
void didPopNext() {
_reloadOpenSession();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const GameTimeLogo(),
actions: [
PopupMenuButton<ThemeMode>(
tooltip: 'Thème',
initialValue: widget.themeMode,
icon: const Icon(Icons.contrast),
onSelected: widget.onThemeModeChanged,
itemBuilder: (context) => const [
PopupMenuItem(value: ThemeMode.system, child: Text('Système')),
PopupMenuItem(value: ThemeMode.light, child: Text('Clair')),
PopupMenuItem(value: ThemeMode.dark, child: Text('Sombre')),
],
),
],
),
body: ListView(
children: [
FutureBuilder<ActiveWorkoutSession?>(
future: _openSession,
builder: (context, snapshot) {
final session = snapshot.data;
if (session == null) {
return const SizedBox.shrink();
}
final plan = WorkoutExecutionPlan.fromSession(session);
final position = ExecutionPosition(
programIndex: session.currentProgramIndex,
exerciseIndex: session.currentExerciseIndex,
setIndex: session.currentSetIndex,
);
final exercise = plan.exerciseAt(position);
return CourtBlazerAccentPanel(
margin: const EdgeInsets.all(12),
child: MaterialBanner(
content: Text(
'Séance en cours — Reprendre · ${exercise.name}, '
'série ${position.setIndex + 1}/${exercise.setsCount}',
),
actions: [
TextButton(
onPressed: () => _resume(session),
child: const Text('Reprendre'),
),
],
),
);
},
),
ListTile(
leading: const Icon(Icons.fitness_center),
title: const Text('Exercices'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ExerciseLibraryScreen(
exerciseUseCases: widget.bootstrap.exerciseUseCases,
mediaUseCases: widget.bootstrap.mediaUseCases,
),
),
),
),
ListTile(
leading: const Icon(Icons.list_alt),
title: const Text('Programmes'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ProgramListScreen(
programUseCases: widget.bootstrap.programUseCases,
exerciseUseCases: widget.bootstrap.exerciseUseCases,
shareUseCases: widget.bootstrap.shareUseCases,
authUseCases: widget.bootstrap.authUseCases,
syncUseCases: widget.bootstrap.syncUseCases,
),
),
),
),
ListTile(
leading: const Icon(Icons.event_note),
title: const Text('Séances'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => WorkoutTemplateListScreen(
workoutTemplateUseCases:
widget.bootstrap.workoutTemplateUseCases,
programUseCases: widget.bootstrap.programUseCases,
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
performanceReferenceUseCase:
widget.bootstrap.exercisePerformanceReferenceUseCase,
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
mediaUseCases: widget.bootstrap.mediaUseCases,
shareUseCases: widget.bootstrap.shareUseCases,
authUseCases: widget.bootstrap.authUseCases,
syncUseCases: widget.bootstrap.syncUseCases,
),
),
),
),
ListTile(
leading: const Icon(Icons.history),
title: const Text('Historique'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => HistoryListScreen(
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
workoutTemplateUseCases:
widget.bootstrap.workoutTemplateUseCases,
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
performanceReferenceUseCase:
widget.bootstrap.exercisePerformanceReferenceUseCase,
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
mediaUseCases: widget.bootstrap.mediaUseCases,
onOpenProgression: () => _openProgression(context),
),
),
),
),
ListTile(
leading: const Icon(Icons.show_chart),
title: const Text('Progression'),
subtitle: const Text(
'Volume, régularité et évolution par exercice',
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _openProgression(context),
),
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,
syncUseCases: widget.bootstrap.syncUseCases,
shareUseCases: widget.bootstrap.shareUseCases,
),
),
),
),
],
),
);
}
Future<void> _openProgression(BuildContext context) {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ProgressionScreen(
progressionStatsUseCase: widget.bootstrap.progressionStatsUseCase,
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
performanceReferenceUseCase:
widget.bootstrap.exercisePerformanceReferenceUseCase,
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
mediaUseCases: widget.bootstrap.mediaUseCases,
),
),
);
}
Future<void> _resume(ActiveWorkoutSession session) async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => WorkoutExecutionScreen(
initialSession: session,
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
performanceReferenceUseCase:
widget.bootstrap.exercisePerformanceReferenceUseCase,
mediaUseCases: widget.bootstrap.mediaUseCases,
),
),
);
if (!mounted) return;
_reloadOpenSession();
}
Future<ActiveWorkoutSession?> _findOpenSession() {
return widget.bootstrap.activeWorkoutSessionUseCases.findOpen();
}
void _reloadOpenSession() {
setState(() {
_openSession = _findOpenSession();
});
}
}
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();
}