feat(tutorial): guide the first launch with spotlighted steps and gesture hands

Ajoute un didacticiel joué à la première utilisation : un voile sombre perce un
trou de lumière autour de l'élément à découvrir, une main animée mime le geste
attendu (tap, appui long, glisser, pincement à deux doigts pour zoomer) et une
bulle explique l'étape avec sa progression.

- visite d'accueil : nouvelle session, télémétrie, barre de navigation, réglages
- visite de l'éditeur d'impacts : ajouter, déplacer, zoomer au pincement, valider
- chaque visite n'est jouée qu'une fois (SharedPreferences)
- Paramètres > Aide & didacticiel > Revoir le didacticiel : réinitialise tout et
  relance la visite au retour sur l'écran concerné

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
qlionbleusam
2026-08-29 11:46:02 +02:00
co-authored by Claude Opus 5
parent 32143c5bb1
commit 32582aba3d
14 changed files with 1566 additions and 1 deletions
+107 -1
View File
@@ -14,6 +14,10 @@ import '../session/session_setup_screen.dart';
import '../session/session_provider.dart';
import '../settings/settings_screen.dart';
import '../statistics/statistics_screen.dart';
import '../tutorial/tutorial_coach.dart';
import '../tutorial/tutorial_provider.dart';
import '../tutorial/tutorial_step.dart';
import '../../services/tutorial_service.dart';
import 'widgets/stats_card.dart';
class HomeScreen extends StatefulWidget {
@@ -33,10 +37,18 @@ class _HomeScreenState extends State<HomeScreen> {
SessionProvider? _sessionProvider;
bool _wasSessionActive = false;
// Clés utilisées par le didacticiel pour mettre les éléments en avant.
final GlobalKey _tutoActionKey = GlobalKey();
final GlobalKey _tutoStatsKey = GlobalKey();
final GlobalKey _tutoSettingsKey = GlobalKey();
TutorialProvider? _tutorialProvider;
@override
void initState() {
super.initState();
_loadStats();
// Première utilisation : la visite guidée démarre dès le premier rendu.
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeStartTutorial());
}
@override
@@ -57,6 +69,89 @@ class _HomeScreenState extends State<HomeScreen> {
_sessionProvider!.addListener(_onSessionChanged);
_wasSessionActive = provider.isSessionActive;
}
final tutorial = context.read<TutorialProvider>();
if (tutorial != _tutorialProvider) {
_tutorialProvider?.removeListener(_onTutorialChanged);
_tutorialProvider = tutorial;
_tutorialProvider!.addListener(_onTutorialChanged);
}
}
/// Les préférences du didacticiel sont lues de façon asynchrone : on
/// retente le démarrage dès qu'elles sont disponibles.
void _onTutorialChanged() {
if (!mounted) return;
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeStartTutorial());
}
/// Lance la visite guidée de l'accueil si elle n'a pas encore été vue
/// (première utilisation, ou didacticiel relancé depuis les paramètres).
void _maybeStartTutorial() {
if (!mounted) return;
if (!(ModalRoute.of(context)?.isCurrent ?? false)) return;
TutorialCoach.maybeStart(
context,
tourId: TutorialTours.home,
stepsBuilder: _buildHomeTutorialSteps,
);
}
List<TutorialStep> _buildHomeTutorialSteps() {
final isSessionActive = context.read<SessionProvider>().isSessionActive;
return [
const TutorialStep(
title: 'Bienvenue dans Bully',
description:
'Ce guide rapide vous montre comment enregistrer une séance, '
'placer vos impacts et suivre vos progrès.\n'
'Touchez l\'écran pour passer à l\'étape suivante.',
icon: Icons.waving_hand_outlined,
),
TutorialStep(
targetKey: _tutoActionKey,
title: isSessionActive ? 'Votre session en cours' : 'Démarrez une session',
description: isSessionActive
? 'Reprenez la session en cours pour photographier une nouvelle '
'cible, ou clôturez-la avec le bouton rouge.'
: 'Tout part d\'ici : créez une session, choisissez votre arme, '
'puis photographiez votre cible.',
gesture: TutorialGesture.tap,
icon: Icons.add_circle_outline,
spotPadding: 10,
),
TutorialStep(
targetKey: _tutoStatsKey,
title: 'Votre télémétrie',
description:
'Sessions, tirs analysés, score moyen et meilleur score se mettent '
'à jour après chaque séance. « Détails » ouvre les statistiques '
'complètes.',
icon: Icons.insights_outlined,
),
TutorialStep(
targetKey: navigationDockKey,
title: 'La barre de navigation',
description:
'Accueil, Historique, Statistiques et Armurerie : vos quatre '
'espaces de travail, accessibles à tout moment.',
gesture: TutorialGesture.tap,
icon: Icons.dashboard_customize_outlined,
spotPadding: 6,
),
TutorialStep(
targetKey: _tutoSettingsKey,
title: 'Paramètres & aide',
description:
'Thème, armurerie, sauvegarde… et le bouton « Revoir le '
'didacticiel » pour rejouer ce guide quand vous le souhaitez.',
gesture: TutorialGesture.tap,
shape: TutorialHighlightShape.circle,
icon: Icons.settings_outlined,
spotPadding: 6,
),
];
}
void _onSessionChanged() {
@@ -70,6 +165,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override
void dispose() {
_sessionProvider?.removeListener(_onSessionChanged);
_tutorialProvider?.removeListener(_onTutorialChanged);
super.dispose();
}
@@ -146,6 +242,7 @@ class _HomeScreenState extends State<HomeScreen> {
),
actions: [
IconButton(
key: _tutoSettingsKey,
icon: const Icon(Icons.settings_outlined),
onPressed: () => _navigateToSettings(context),
tooltip: 'Paramètres',
@@ -205,7 +302,10 @@ class _HomeScreenState extends State<HomeScreen> {
children: [
_buildHeader(isDark, primaryColor),
const SizedBox(height: 16),
_buildMainActionSection(context, isDark, primaryColor),
KeyedSubtree(
key: _tutoActionKey,
child: _buildMainActionSection(context, isDark, primaryColor),
),
const SizedBox(height: 22),
if (_isLoading)
const Padding(
@@ -596,6 +696,7 @@ class _HomeScreenState extends State<HomeScreen> {
),
const SizedBox(height: 12),
Row(
key: _tutoStatsKey,
children: [
Expanded(
child: InkWell(
@@ -985,6 +1086,11 @@ class _HomeScreenState extends State<HomeScreen> {
context,
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
if (!mounted) return;
_loadStats();
// Si le didacticiel a été relancé depuis les paramètres, il redémarre
// une fois la transition de retour terminée.
await Future<void>.delayed(const Duration(milliseconds: 350));
_maybeStartTutorial();
}
}