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
@@ -25,6 +25,9 @@ import 'package:provider/provider.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/shot.dart';
import '../../services/tutorial_service.dart';
import '../tutorial/tutorial_coach.dart';
import '../tutorial/tutorial_step.dart';
import 'analysis_provider.dart';
import 'widgets/target_overlay.dart';
@@ -43,12 +46,71 @@ class _ImpactEditorScreenState extends State<ImpactEditorScreen> {
double _currentZoomScale = 1.0;
String? _movingShotId;
// Clés du didacticiel : zone de travail et bouton de validation.
final GlobalKey _tutoCanvasKey = GlobalKey();
final GlobalKey _tutoValidateKey = GlobalKey();
@override
void initState() {
super.initState();
_transformationController.addListener(_onTransformChanged);
// Première ouverture de l'éditeur : on montre les gestes disponibles.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
TutorialCoach.maybeStart(
context,
tourId: TutorialTours.impactEditor,
stepsBuilder: _buildEditorTutorialSteps,
);
});
}
List<TutorialStep> _buildEditorTutorialSteps() => [
TutorialStep(
targetKey: _tutoCanvasKey,
title: 'Ajouter un impact',
description:
'Touchez la cible à l\'endroit de l\'impact : il est ajouté '
'immédiatement, même collé à un impact déjà placé. Le score est '
'calculé automatiquement selon la zone touchée.',
gesture: TutorialGesture.tap,
icon: Icons.add_location_alt_outlined,
spotPadding: 0,
),
TutorialStep(
targetKey: _tutoCanvasKey,
title: 'Déplacer un impact',
description:
'Appui long sur un impact, puis glissez le doigt pour l\'ajuster '
'au millimètre. L\'impact reste visible au-dessus du doigt.',
gesture: TutorialGesture.drag,
icon: Icons.open_with,
spotPadding: 0,
),
TutorialStep(
targetKey: _tutoCanvasKey,
title: 'Zoomer sur la cible',
description:
'Écartez deux doigts pour zoomer (jusqu\'à 12×) et placer vos '
'impacts avec précision ; rapprochez-les pour dézoomer. À un '
'doigt, vous faites glisser l\'image.',
gesture: TutorialGesture.pinch,
icon: Icons.zoom_in,
spotPadding: 0,
),
TutorialStep(
targetKey: _tutoValidateKey,
title: 'Valider vos impacts',
description:
'VALIDER renvoie vers la synthèse avec les scores et le '
'groupement. La corbeille, à gauche, efface tous les impacts '
'sans toucher à la calibration.',
gesture: TutorialGesture.tap,
icon: Icons.check_circle_outline,
spotPadding: 8,
),
];
@override
void dispose() {
_transformationController.removeListener(_onTransformChanged);
@@ -123,6 +185,7 @@ class _ImpactEditorScreenState extends State<ImpactEditorScreen> {
),
const SizedBox(width: 12),
FloatingActionButton.extended(
key: _tutoValidateKey,
heroTag: 'validate_impacts',
onPressed: () => Navigator.pop(context, true),
backgroundColor: AppTheme.primaryColor,
@@ -147,6 +210,7 @@ class _ImpactEditorScreenState extends State<ImpactEditorScreen> {
// Zone image plein écran : InteractiveViewer dans un body nu.
Expanded(
key: _tutoCanvasKey,
child: InteractiveViewer(
transformationController: _transformationController,
minScale: 1.0,