- F1: "Sauvegarde locale" panel in Profil (both signed-out/signed-in states, positioned per UX cadrage), "Exporter mes données" calling DataExportUseCase.exportAll() then handing the file to the system share sheet, with the exact UX success/error snackbars. - F2: "Importer des données" picks a file, previews it, then confirms with the exact UX dialogs: simplified single-choice when there is no local data yet, Fusionner/Remplacer tout (with a destructive second confirmation) otherwise. All typed LocalBackupValidationError cases are mapped to their dedicated dialog copy, including the active workout session block.
300 lines
11 KiB
Dart
300 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,
|
|
dataExportUseCase: widget.bootstrap.dataExportUseCase,
|
|
dataImportUseCase: widget.bootstrap.dataImportUseCase,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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();
|
|
}
|