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
@@ -0,0 +1,54 @@
/// État global du didacticiel : quelles visites guidées restent à jouer.
///
/// Le provider est chargé au démarrage ; tant que les préférences ne sont pas
/// lues, aucune visite n'est déclenchée (évite un flash d'overlay au lancement).
library;
import 'package:flutter/foundation.dart';
import '../../services/tutorial_service.dart';
class TutorialProvider with ChangeNotifier {
TutorialProvider({TutorialService? service})
: _service = service ?? TutorialService() {
load();
}
final TutorialService _service;
final Set<String> _completed = {};
bool _loaded = false;
/// `true` une fois les préférences lues.
bool get isLoaded => _loaded;
/// `true` si l'utilisateur a déjà terminé (ou passé) la visite d'accueil.
bool get hasSeenIntro => _completed.contains(TutorialTours.home);
/// Lit les visites déjà vues. Les visites terminées pendant le chargement
/// sont conservées (le résultat est fusionné, jamais écrasé).
Future<void> load() async {
_completed.addAll(await _service.loadCompletedTours());
_loaded = true;
notifyListeners();
}
/// Faut-il jouer la visite [tourId] ?
bool shouldRun(String tourId) => _loaded && !_completed.contains(tourId);
/// Marque une visite comme vue (terminée ou passée) : elle ne rejouera plus.
Future<void> complete(String tourId) async {
if (_completed.add(tourId)) {
notifyListeners();
await _service.markCompleted(tourId);
}
}
/// Relance le didacticiel depuis les paramètres : toutes les visites
/// redeviennent disponibles et rejoueront dès l'affichage de leur écran.
Future<void> restart() async {
await _service.resetAll();
_completed.clear();
_loaded = true;
notifyListeners();
}
}