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:
co-authored by
Claude Opus 5
parent
32143c5bb1
commit
32582aba3d
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user