docs: cadrage initial Storytime (specs par jalon, roadmap, CLAUDE.md)

Lecteur d'histoires cadenassé pour le coucher (Android/Flutter).
- CLAUDE.md : principes craftsmanship/TDD/clean code/clean archi + decisions techniques
- ROADMAP.md : suivi haut niveau des 7 jalons, a tenir a jour par etape
- docs/specs/ : specs completes decoupees par jalon, etapes en sous-fichiers
- .gitignore Flutter (pubspec.lock versionne, projet applicatif)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vincent Bourdon
2026-06-19 17:03:33 +02:00
commit 16fd4c8c36
32 changed files with 1360 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# Vision & architecture
## Vision produit
Storytime transforme une tablette Android en **lecteur d'histoires du soir cadenassé**.
L'enfant choisit et écoute des histoires (épisodes de podcasts) sans pouvoir sortir
de l'application ni accéder au reste de la tablette. Le parent garde le contrôle :
il sélectionne les sources, définit un code parental et fixe des limites (temps /
nombre d'histoires) adaptées au rituel du coucher.
### Utilisateurs
- **Enfant** (sait lire, choisit par le titre) : interface minimale, gros boutons.
- **Parent** : configuration protégée par code 4 chiffres.
### Périmètre v1
Inclus : lecture en streaming, recherche annuaire + ajout RSS, liste d'histoires,
épinglage, code parental, limites (temps + nombre) avec avertissements doux.
Exclus v1 : téléchargement hors-ligne, multi-profils enfants, multi-plateforme,
récupération de code par question secrète (réinstaller réinitialise).
## Architecture cible
Clean Architecture, feature-first. Détail des couches et de la règle de dépendance
dans [`../../CLAUDE.md`](../../CLAUDE.md) §3-4. Rappel du flux :
```
Presentation (Riverpod/UI) → Application (use cases) → Domain ← Data (infra)
```
### Features et leurs responsabilités
| Feature | Responsabilité | Dépendances externes (couche data) |
|---------|----------------|-------------------------------------|
| `locking` | Épingler/désépingler l'app, état de verrouillage | plugin `kiosk_mode` / channel Kotlin |
| `playback` | Lire un épisode, pause, suivant, progression | `just_audio`, `audio_service` |
| `podcasts` | Rechercher, ajouter (RSS), persister les abonnements, lister les épisodes | iTunes Search, `dart_rss`, SQLite |
| `parental` | Créer/vérifier le code, garder l'accès parent | `flutter_secure_storage` |
| `limits` | Compter temps & histoires, avertir, arrêter, reset quotidien | `shared_preferences` |
### Flux de données type (lecture d'une histoire)
1. `presentation` (écran enfant) lit la liste via `ListEpisodesUseCase`.
2. L'enfant tape un titre → `PlayStoryUseCase` (application) demande au `PlaybackRepository` (domain) de jouer ; l'impl. (data) pilote `just_audio`.
3. `limits` observe les événements de lecture (histoire démarrée, temps écoulé) et déclenche avertissements / arrêt.
4. `locking` maintient l'épinglage tout au long.
## Principes transverses
- **Result/Failure** plutôt qu'exceptions traversantes (cf. CLAUDE.md §4).
- **Immuabilité** des entités et états.
- **DI par Riverpod** ; tout est *overridable* en test.
- **Messages enfant rassurants**, jamais d'erreur technique brute à l'écran enfant.
- **Honnêteté de l'UI sur le verrouillage** : ne pas laisser croire à une protection plus forte que ce que l'épinglage natif offre réellement.
## Ordre de construction & dépendances
- **J0** pose le squelette → prérequis de tout.
- **J1** valide l'épinglage → **bloquant** : conditionne la viabilité du produit.
- **J2** lecture audio (testable avec un flux en dur).
- **J3** podcasts (recherche + RSS + persistance).
- **J4** espace enfant : consomme J2 (lecture) + J3 (liste) + J1 (verrouillage).
- **J5** code parental + espace parent : **abrite** la gestion podcasts de J3 derrière le code.
- **J6** limites : s'appuie sur les événements de J2 et l'arrêt/reprise gérés avec J5.
> Note de dépendance J3↔J5 : J3 construit le domaine podcasts + un écran d'accès
> temporaire (dev) ; J5 déplace cet accès derrière la porte parentale. Documenté
> dans les specs concernées.
+38
View File
@@ -0,0 +1,38 @@
# Specs Storytime
Spécifications du projet, **découpées par jalon**. Un dossier par jalon ; chaque
jalon contient un `README.md` (objectif, périmètre, Definition of Done) et ses
**étapes d'implémentation en sous-fichiers numérotés**.
Le suivi d'avancement est dans [`../../ROADMAP.md`](../../ROADMAP.md) (à tenir à jour).
Les principes d'ingénierie sont dans [`../../CLAUDE.md`](../../CLAUDE.md).
## Organisation
| Fichier | Rôle |
|---------|------|
| [`00-vision-architecture.md`](00-vision-architecture.md) | Vision produit, architecture cible, conventions transverses |
| `jalon-N-xxx/README.md` | Objectif du jalon + critères de fin |
| `jalon-N-xxx/NN-etape.md` | Une étape d'implémentation (conception + plan TDD + DoD) |
## Format d'un fichier d'étape
Chaque sous-fichier d'étape suit la même trame :
1. **Objectif** — ce que l'étape livre, en une phrase.
2. **Périmètre & hors-périmètre** — ce qui est inclus / explicitement exclu (YAGNI).
3. **Dépendances** — étapes/jalons préalables.
4. **Conception** — couches concernées, interfaces/contrats, modèles.
5. **Plan TDD** — la liste des tests à écrire **d'abord** (Red), dans l'ordre.
6. **Definition of Done** — conditions de complétion (tests verts, lint, roadmap à jour).
7. **Risques / notes** — pièges connus.
## Jalons
0. [Fondations](jalon-0-fondations/)
1. [Verrouillage / épinglage](jalon-1-verrouillage/) ⚠️ bloquant
2. [Lecture audio](jalon-2-lecture-audio/)
3. [Découverte & gestion des podcasts](jalon-3-podcasts/)
4. [Espace enfant](jalon-4-espace-enfant/)
5. [Code parental & espace parent](jalon-5-code-parental/)
6. [Limites, avertissements & fin de session](jalon-6-limites/)
@@ -0,0 +1,37 @@
# 0.1 — Structure du projet & arborescence
## Objectif
Créer le projet Flutter Android et l'arborescence clean architecture cible.
## Périmètre & hors-périmètre
- Inclus : `flutter create` (Android only), arborescence `lib/core` + `lib/features/*`, `main.dart` minimal, dépendances de base au `pubspec.yaml`.
- Exclus : toute logique métier.
## Dépendances
Aucune (première étape).
## Conception
- Cibler Android uniquement (`--platforms=android`).
- Arborescence (cf. CLAUDE.md §4) :
```
lib/core/{error,theme,router,di}
lib/features/{locking,playback,podcasts,parental,limits}
```
Chaque feature reçoit les sous-dossiers `domain/ application/ data/ presentation/` (placeholders avec un `.gitkeep` ou un fichier barrel vide).
- `pubspec.yaml` : ajouter `flutter_riverpod`. Les autres paquets (`just_audio`, `kiosk_mode`, …) seront ajoutés **au jalon qui les utilise** (YAGNI).
- `main.dart` : `ProviderScope` + `MaterialApp` avec un écran d'accueil placeholder.
## Plan TDD
La création de squelette est surtout structurelle ; le test porte sur le démarrage :
1. **Red** : test de widget `app_boots_test.dart` — `pumpWidget(StorytimeApp())` attend de trouver le placeholder (texte « Storytime »). Échoue tant que l'app n'existe pas.
2. **Green** : créer `StorytimeApp` + écran placeholder.
3. **Refactor** : extraire le thème/router si nécessaire (sera étoffé en 0.3).
## Definition of Done
- `flutter run` démarre l'app.
- `app_boots_test.dart` vert.
- Arborescence conforme.
- Étape 0.1 cochée dans `ROADMAP.md`.
## Risques / notes
- Vérifier la version Flutter/Dart utilisée et la consigner (ex. dans le README projet) pour reproductibilité.
@@ -0,0 +1,35 @@
# 0.2 — Outillage qualité
## Objectif
Garantir un code uniformément formaté, sans warning, et vérifiable d'une commande.
## Périmètre & hors-périmètre
- Inclus : lint strict, format, script de vérification locale, conventions de test.
- Exclus : CI distante (GitHub Actions, etc.) — peut venir plus tard, hors v1.
## Dépendances
0.1 (projet existant).
## Conception
- `analysis_options.yaml` : partir de `flutter_lints`, puis durcir (ex. `prefer_const_constructors`, `always_declare_return_types`, `require_trailing_commas`, interdiction de `print`). Objectif **0 warning**.
- Ajouter `mocktail` en `dev_dependencies` (mocks pour les tests à venir).
- **Script « CI locale »** `tool/check.sh` enchaînant :
1. `dart format --set-exit-if-changed .`
2. `flutter analyze`
3. `flutter test`
Code de sortie non nul si l'une échoue.
- Documenter dans le README projet : « avant de cocher une étape, `tool/check.sh` doit passer ».
## Plan TDD
Étape outillage : la « preuve » est l'exécution du script.
1. Écrire `tool/check.sh`.
2. Le lancer : il doit passer sur le squelette de 0.1 (format OK, analyze 0 issue, test 0.1 vert).
3. Vérifier qu'il **échoue** volontairement si on introduit un fichier mal formaté (test du garde-fou), puis rétablir.
## Definition of Done
- `tool/check.sh` passe intégralement.
- `flutter analyze` : 0 issue.
- Étape 0.2 cochée dans `ROADMAP.md`.
## Risques / notes
- Garder le lint strict mais pragmatique : si une règle gêne sans valeur, la désactiver explicitement avec un commentaire justificatif plutôt que de la subir.
@@ -0,0 +1,36 @@
# 0.3 — Socle transverse
## Objectif
Fournir les briques transverses réutilisées par toutes les features : gestion
d'erreur typée, thème, navigation, et amorce de DI Riverpod.
## Périmètre & hors-périmètre
- Inclus : `Result`/`Failure`, thème Material 3 (palette douce « coucher »), router, organisation DI.
- Exclus : providers métier (créés par chaque feature).
## Dépendances
0.1, 0.2.
## Conception
- **`core/error/`** :
- `Failure` (classe scellée/sealed) avec sous-types de base : `NetworkFailure`, `NotFoundFailure`, `UnexpectedFailure`. Les features ajouteront les leurs.
- `Result<S>` : type `sealed` `Ok<S>(value)` / `Err(Failure)`, avec helpers (`map`, `when`, `fold`). (Ou adopter `dartz`/`fpdart` `Either` — choisir et figer dans CLAUDE.md §7.)
- **`core/theme/`** : `AppTheme` Material 3, couleurs douces, gros composants tactiles (cible enfant), contrastes lisibles le soir.
- **`core/router/`** : router (`go_router` ou Navigator 2 simple) avec routes nommées : `child` (défaut), `parentGate`, `parent`. Pas encore d'écrans réels.
- **`core/di/`** : conventions d'organisation des providers (un fichier `providers.dart` par feature, agrégés). `ProviderScope` racine dans `main.dart`.
## Plan TDD
1. **Red** : `result_test.dart``Ok(1).map((v) => v+1)` donne `Ok(2)` ; `Err(f).map(...)` reste `Err(f)` ; `fold`/`when` aiguillent correctement. Échoue (type absent).
2. **Green** : implémenter `Result`/`Failure`.
3. **Red** : `theme_test.dart` (widget) — un `MaterialApp` avec `AppTheme` expose `useMaterial3 == true` et la couleur primaire attendue.
4. **Green** : implémenter `AppTheme`.
5. **Refactor** : nettoyer, documenter les helpers.
## Definition of Done
- Tests `result_test.dart` et `theme_test.dart` verts.
- App démarre avec le thème appliqué et le router en place (route `child` placeholder).
- `tool/check.sh` passe.
- Étape 0.3 cochée dans `ROADMAP.md` ; choix `Result` maison vs `fpdart` consigné dans CLAUDE.md §7.
## Risques / notes
- Ne pas sur-concevoir le `Result` : juste ce que les use cases consommeront. Étoffer au besoin réel.
+27
View File
@@ -0,0 +1,27 @@
# Jalon 0 — Fondations
## Objectif
Poser un projet Flutter sain : arborescence clean architecture, outillage qualité
strict, et le socle transverse (gestion d'erreur, thème, navigation, DI) sur lequel
tous les autres jalons s'appuient.
## Périmètre
- Création du projet Flutter Android.
- Arborescence `core/` + `features/` (dossiers vides prêts à recevoir les features).
- Lint strict, format, script de vérification local (« CI locale »).
- Type `Result`/`Failure`, thème Material 3, router, conteneur Riverpod.
## Hors-périmètre
Aucune feature métier. Pas d'UI fonctionnelle au-delà d'un écran d'accueil placeholder.
## Étapes
1. [0.1 — Structure du projet & arborescence](01-structure-projet.md)
2. [0.2 — Outillage qualité](02-outillage-qualite.md)
3. [0.3 — Socle transverse](03-socle-transverse.md)
## Definition of Done (jalon)
- `flutter run` démarre l'app (écran placeholder) sur émulateur/appareil.
- `flutter analyze` : 0 issue. `dart format --set-exit-if-changed` : OK.
- Tests du socle transverse verts.
- Arborescence conforme à CLAUDE.md §4.
- `ROADMAP.md` : étapes 0.1→0.3 cochées.
@@ -0,0 +1,44 @@
# 1.1 — Spike `kiosk_mode` sur appareil réel ⚠️ BLOQUANT
## Objectif
Prouver, sur la tablette cible, qu'on peut épingler/désépingler l'app par code et
que l'épinglage tient face à un usage enfant. C'est un **spike** : on cherche la
preuve de faisabilité, pas le code définitif.
## Périmètre & hors-périmètre
- Inclus : intégrer `kiosk_mode`, déclencher l'épinglage au démarrage, bouton de désépinglage (temporaire), observation du flux Android (confirmation, PIN au désépinglage).
- Exclus : architecture propre (vient en 1.2), UI finale.
## Dépendances
Jalon 0.
## Conception (jetable)
- Ajouter `kiosk_mode` au `pubspec.yaml`.
- Écran de spike : bouton « Épingler » → `startKioskMode()` (ou API équivalente du plugin), affichage de l'état (`getKioskMode()` / stream), bouton « Désépingler ».
- Tester manuellement les réglages Android : épinglage activé, « exiger le PIN au désépinglage » ON/OFF.
## Protocole de validation (sur appareil réel — pas seulement émulateur)
1. Lancer, appuyer « Épingler ». Vérifier que l'app est épinglée.
2. Tenter de sortir : geste accueil, multitâche, retour. → doit être bloqué.
3. Désépingler via le geste système → doit exiger le PIN Android (si option activée).
4. Couper/rallumer l'écran : vérifier le comportement.
5. Redémarrer la tablette : confirmer que l'épinglage ne survit pas (limite connue, à documenter).
## Définition du résultat
- **Succès** → on passe à 1.2, et 1.3 devient « N/A » (noter dans roadmap).
- **Échec / instable** → on documente précisément l'échec et on passe à 1.3 (fallback natif). Si 1.3 échoue aussi → escalade matérielle (cf. README jalon).
## Plan de test
Spike = validation **manuelle documentée** (impossible d'automatiser l'épinglage système de façon fiable hors appareil). Consigner les résultats du protocole dans ce fichier (section « Résultats » ci-dessous).
## Definition of Done
- Protocole exécuté sur la tablette cible, résultats consignés.
- Décision plugin vs fallback prise.
- Étape 1.1 cochée dans `ROADMAP.md` + entrée dans le journal des décisions.
## Résultats (à remplir lors de l'exécution)
> _Tablette : … / Android : … / version plugin : …_
> _Observations : …_
## Risques / notes
- Comportements variables selon constructeur/version Android : c'est exactement ce que ce spike doit révéler tôt.
@@ -0,0 +1,45 @@
# 1.2 — Service de verrouillage (domaine + use cases)
## Objectif
Encapsuler l'épinglage derrière une interface de domaine propre et des use cases
testables, indépendants de l'implémentation (plugin ou natif).
## Périmètre & hors-périmètre
- Inclus : interface `LockingRepository`, use cases, impl. data branchée sur le résultat de 1.1, providers Riverpod.
- Exclus : UI enfant (jalon 4) ; c'est elle qui appellera ces use cases.
## Dépendances
1.1 (faisabilité prouvée).
## Conception
- **Domain** (`features/locking/domain/`) :
- `LockState` (value object / enum) : `unlocked`, `locked`, `unsupported`.
- `LockingRepository` (interface) :
- `Future<Result<Unit>> startLock()`
- `Future<Result<Unit>> stopLock()`
- `Stream<LockState> watchState()`
- `Future<LockState> currentState()`
- `LockingFailure` (sous-type de `Failure`) : `LockUnsupportedFailure`, `LockDeniedFailure`.
- **Application** (`features/locking/application/`) :
- `StartLockUseCase`, `StopLockUseCase`, `WatchLockStateUseCase`.
- `StopLockUseCase` **n'autorise pas** la sortie système ; il ne fait que demander l'arrêt de l'épinglage (la confirmation reste au système). À documenter.
- **Data** (`features/locking/data/`) :
- `KioskModeLockingRepository` (impl. via plugin) **ou** `NativeChannelLockingRepository` (impl. 1.3), selon décision de 1.1.
- **DI** : provider `lockingRepositoryProvider` (override en test).
## Plan TDD
1. **Red** : `start_lock_use_case_test.dart` — avec un `LockingRepository` mocké renvoyant `Ok`, le use case renvoie `Ok` ; en `Err(LockDenied)`, propage l'échec. Échoue (types absents).
2. **Green** : créer interface + use case.
3. **Red** : `watch_lock_state_use_case_test.dart` — le stream relaie les états émis par le repo mocké.
4. **Green** : implémenter.
5. **Red** : `kiosk_mode_locking_repository_test.dart` — avec un wrapper mockable autour du plugin, `startLock` mappe succès→`Ok` et exception→`LockingFailure`.
6. **Green** : implémenter l'impl. data avec mapping d'erreurs.
7. **Refactor**.
## Definition of Done
- Tous les tests ci-dessus verts ; aucune dépendance au plugin dans `domain/`/`application/`.
- `tool/check.sh` passe.
- Étape 1.2 cochée dans `ROADMAP.md`.
## Risques / notes
- Garder l'API du repository minimale : start/stop/observe. Pas de fioritures.
@@ -0,0 +1,43 @@
# 1.3 — Fallback natif Kotlin (platform channel)
## Objectif
Si le plugin `kiosk_mode` s'est révélé insuffisant en 1.1, fournir une
implémentation native Kotlin du verrouillage via un platform channel, derrière la
même interface `LockingRepository`.
## Condition d'exécution
**Étape conditionnelle.** À réaliser uniquement si 1.1 a conclu que le plugin ne
tient pas. Sinon : marquer « N/A » dans la roadmap et passer au jalon 2.
## Périmètre & hors-périmètre
- Inclus : `MethodChannel` Dart↔Kotlin, code Kotlin appelant `Activity.startLockTask()`/`stopLockTask()`, impl. `NativeChannelLockingRepository`.
- Exclus : Device Owner / mode kiosque complet (relève d'un changement de stratégie matérielle, pas de cette étape).
## Dépendances
1.1 (échec plugin), 1.2 (interface existante).
## Conception
- **Côté Android** (`android/app/src/main/kotlin/...`) :
- `MethodChannel("storytime/locking")` avec méthodes `startLock`, `stopLock`, `currentState`.
- Appeler `startLockTask()` / `stopLockTask()` sur l'`Activity`. Sans Device Owner, cela déclenche le **Screen Pinning** standard (confirmation système).
- Gérer les exceptions et renvoyer un code d'erreur exploitable côté Dart.
- **Côté Dart** (`features/locking/data/`) :
- `NativeChannelLockingRepository implements LockingRepository`, mappant les réponses du channel vers `Result`/`LockingFailure`.
- Brancher `lockingRepositoryProvider` sur cette impl. à la place de la version plugin.
## Plan TDD
1. **Red** : `native_channel_locking_repository_test.dart` — en mockant `MethodChannel` (via `TestDefaultBinaryMessengerBinding`), `startLock` qui répond OK → `Ok` ; qui lève `PlatformException``LockingFailure`.
2. **Green** : implémenter le repository Dart + mapping.
3. **Validation manuelle** : reprendre le protocole de 1.1 avec l'impl. native (épingler/sortir/désépingler/PIN).
4. **Refactor**.
> Le code Kotlin lui-même est validé par le test d'intégration manuel (protocole 1.1) ; le test unitaire Dart couvre le mapping et la frontière du channel.
## Definition of Done
- Test du repository natif vert ; protocole de 1.1 repassé avec succès sur la tablette.
- `lockingRepositoryProvider` pointe sur l'impl. retenue.
- `tool/check.sh` passe.
- Étape 1.3 cochée dans `ROADMAP.md` (ou « N/A » si non nécessaire) + décision au journal.
## Risques / notes
- Si même `startLockTask()` natif ne satisfait pas le besoin sans Device Owner → **escalade** : revenir à l'utilisateur sur le choix « tablette dédiée / mode kiosque », comme acté au cadrage.
+28
View File
@@ -0,0 +1,28 @@
# Jalon 1 — Verrouillage / épinglage ⚠️ BLOQUANT
## Objectif
Valider et implémenter la capacité de l'app à **empêcher la sortie facile** via le
Screen Pinning natif Android, et exposer cette capacité au reste de l'app par une
interface de domaine propre.
## Pourquoi bloquant
C'est l'exigence n°1 du produit. Tant que l'épinglage n'est pas prouvé sur la
tablette cible, on n'investit pas dans le reste. Si ni le plugin ni le fallback
natif ne tiennent, **on remonte au choix matériel** (tablette dédiée / mode kiosque
Device Owner) avant d'aller plus loin.
## Rappel des limites (à respecter dans l'UI)
- La sortie réelle dépend du mécanisme natif Android + PIN tablette, pas du code parental.
- L'épinglage ne survit pas à un redémarrage.
- Côté parent : prévoir un rappel pour activer « épinglage » + « exiger le PIN au désépinglage » dans les réglages Android.
## Étapes
1. [1.1 — Spike `kiosk_mode` sur appareil réel](01-spike-kiosk-mode.md) — **bloquant**
2. [1.2 — Service de verrouillage (domaine + use cases)](02-service-verrouillage.md)
3. [1.3 — Fallback natif Kotlin (platform channel)](03-fallback-natif-kotlin.md)
## Definition of Done (jalon)
- Sur la tablette cible : l'app s'épingle, l'enfant ne peut pas sortir par les gestes normaux, le désépinglage exige le PIN Android.
- `LockingRepository` + use cases `StartLockUseCase`/`StopLockUseCase`/`IsLockedUseCase` testés (impl. mockée).
- Décision tranchée : plugin suffisant **ou** fallback natif retenu — consignée dans le journal de la roadmap.
- `ROADMAP.md` : 1.1→1.3 cochées (1.3 « N/A » si le plugin suffit).
@@ -0,0 +1,43 @@
# 2.1 — Domaine de lecture
## Objectif
Modéliser le cœur métier de la lecture : ce qu'est une histoire jouable et l'état
de lecture, indépendamment de toute techno audio.
## Périmètre & hors-périmètre
- Inclus : entités, value objects, interface `PlaybackRepository`, use cases.
- Exclus : implémentation audio (2.2), UI (2.3).
## Dépendances
Jalon 0.
## Conception
- **Domain** (`features/playback/domain/`) :
- `Episode` (entité) : `id`, `title`, `audioUrl`, `duration?`, `artworkUrl?`. Immuable.
- `PlaybackState` (value object) : `status` (`idle`/`loading`/`playing`/`paused`/`completed`/`error`), `current` (`Episode?`), `position`, `bufferedPosition`.
- `PlaybackRepository` (interface) :
- `Future<Result<Unit>> load(List<Episode> queue, {int startIndex})`
- `Future<Result<Unit>> play()` / `pause()` / `next()` / `previous()`
- `Future<Result<Unit>> seek(Duration position)`
- `Stream<PlaybackState> watch()`
- `PlaybackFailure` : `AudioSourceFailure`, `PlaybackUnavailableFailure`.
- **Application** (`features/playback/application/`) :
- `PlayStoryUseCase` (charge la file + démarre à l'index voulu).
- `TogglePlayPauseUseCase`, `SkipNextUseCase`, `SkipPreviousUseCase`, `SeekUseCase`.
- `WatchPlaybackStateUseCase`.
## Plan TDD
1. **Red** : `playback_state_test.dart``copyWith`, transitions et égalité (immuabilité) de `PlaybackState`.
2. **Green** : implémenter entités/value objects.
3. **Red** : `play_story_use_case_test.dart` — avec `PlaybackRepository` mocké : charge la file et appelle `play` ; index hors bornes → `Err`.
4. **Green** : implémenter le use case.
5. **Red** : use cases toggle/skip/seek — vérifient l'appel correct au repo selon l'état.
6. **Green** : implémenter.
7. **Refactor**.
## Definition of Done
- Tests domaine + use cases verts, sans import Flutter/just_audio.
- `tool/check.sh` passe ; étape 2.1 cochée dans `ROADMAP.md`.
## Risques / notes
- `Episode` est volontairement minimal ; J3 fournira de quoi le construire depuis un flux RSS. Garder le mapping RSS→Episode dans la feature `podcasts`, pas ici.
@@ -0,0 +1,37 @@
# 2.2 — Service audio (just_audio + audio_service)
## Objectif
Implémenter `PlaybackRepository` avec `just_audio` pour le streaming et
`audio_service` pour la lecture en arrière-plan / contrôles écran verrouillé.
## Périmètre & hors-périmètre
- Inclus : impl. concrète du repository, configuration `audio_service`, mapping états plugin → `PlaybackState`, mapping erreurs → `PlaybackFailure`.
- Exclus : domaine (2.1), UI (2.3).
## Dépendances
2.1.
## Conception
- **Data** (`features/playback/data/`) :
- `JustAudioPlaybackRepository implements PlaybackRepository`.
- Encapsule un `AudioPlayer` (just_audio) + une `ConcatenatingAudioSource` pour la file.
- Expose `watch()` en combinant les streams just_audio (`playerStateStream`, `positionStream`, `currentIndexStream`) → `PlaybackState`.
- Capture les erreurs de source (URL invalide, réseau) → `AudioSourceFailure`.
- Intégration `audio_service` : `AudioHandler` reliant les commandes système (notification, casque) au repository.
- **DI** : `playbackRepositoryProvider` (override en test par un fake).
- **Android** : permissions/manifest requis par `audio_service` (service de premier plan, etc.).
## Plan TDD
1. **Red** : `just_audio_playback_repository_test.dart` — en abstrayant `AudioPlayer` derrière une fine façade mockable : `load` configure la source et `play` délègue ; une exception de source → `AudioSourceFailure`. Le mapping des streams vers `PlaybackState` est vérifié sur des événements simulés.
2. **Green** : implémenter la façade + le repository + le mapping.
3. **Validation manuelle** : lire une URL audio de test sur l'appareil (la lecture réelle ne s'automatise pas de façon fiable en unit test) — consigner le résultat.
4. **Refactor**.
## Definition of Done
- Test du repository vert (façade mockée) ; lecture réelle vérifiée manuellement.
- Lecture en arrière-plan fonctionnelle (écran éteint = audio continue).
- `tool/check.sh` passe ; étape 2.2 cochée dans `ROADMAP.md`.
## Risques / notes
- `audio_service` impose une configuration Android précise (manifest, init). Prévoir un temps de mise au point.
- Garder la frontière nette : aucune logique métier dans cette couche, uniquement adaptation au plugin.
@@ -0,0 +1,34 @@
# 2.3 — UI lecteur + contrôleur Riverpod
## Objectif
Offrir un lecteur utilisable par l'enfant : gros boutons lecture/pause,
précédent/suivant, barre de progression, titre de l'histoire en cours.
## Périmètre & hors-périmètre
- Inclus : contrôleur Riverpod (`AsyncNotifier`/`Notifier`) consommant les use cases, widget lecteur.
- Exclus : la liste des histoires (jalon 4) ; ici on teste avec une file en dur.
## Dépendances
2.1, 2.2.
## Conception
- **Presentation** (`features/playback/presentation/`) :
- `PlaybackController` (Riverpod `Notifier`) : expose `PlaybackState`, méthodes `play/pause/next/previous/seek` déléguant aux use cases ; s'abonne à `WatchPlaybackStateUseCase`.
- `PlayerView` (widget) : grand bouton central play/pause (icône claire), boutons précédent/suivant, `Slider` de progression, titre + vignette.
- Composants tactiles larges, lisibles le soir (thème de 0.3).
- Pas de logique métier dans le widget : tout passe par le contrôleur → use cases.
## Plan TDD
1. **Red** : `playback_controller_test.dart` — avec use cases mockés, `play()` appelle `TogglePlayPauseUseCase` ; l'état du contrôleur reflète les `PlaybackState` émis.
2. **Green** : implémenter le contrôleur.
3. **Red** : `player_view_test.dart` (widget) — tap sur le bouton play déclenche `controller.play` (provider overridé par un mock) ; l'icône bascule play/pause selon l'état ; le titre s'affiche.
4. **Green** : implémenter le widget.
5. **Refactor**.
## Definition of Done
- Tests contrôleur + widget verts.
- Démo manuelle : avec une file en dur, lecture/pause/suivant/seek fonctionnent et l'UI suit.
- `tool/check.sh` passe ; étape 2.3 cochée dans `ROADMAP.md`.
## Risques / notes
- Soigner l'accessibilité tactile (taille des cibles) dès maintenant : c'est l'écran que l'enfant manipule.
@@ -0,0 +1,25 @@
# Jalon 2 — Lecture audio
## Objectif
Lire un épisode audio en streaming : lecture, pause, suivant/précédent, position,
le tout derrière une interface de domaine propre et pilotable depuis l'UI.
## Périmètre
- Lecture d'une URL audio (streaming).
- Contrôles : play/pause, suivant/précédent, seek, progression.
- Lecture en arrière-plan / écran verrouillé (`audio_service`).
- Testable avec un flux/épisode « en dur » (pas besoin de J3).
## Hors-périmètre
- Téléchargement hors-ligne (exclu v1).
- File d'attente avancée / aléatoire : on garde une liste ordonnée simple.
## Étapes
1. [2.1 — Domaine de lecture](01-domaine-lecture.md)
2. [2.2 — Service audio (just_audio + audio_service)](02-service-audio.md)
3. [2.3 — UI lecteur + contrôleur Riverpod](03-ui-lecteur.md)
## Definition of Done (jalon)
- Un épisode de test se lit, se met en pause, avance/recule ; la progression s'affiche.
- Domain/use cases testés sans dépendance Flutter/audio.
- `tool/check.sh` passe ; `ROADMAP.md` 2.1→2.3 cochées.
@@ -0,0 +1,38 @@
# 3.1 — Recherche annuaire (iTunes Search)
## Objectif
Permettre au parent de rechercher un podcast par mots-clés et d'obtenir une liste
de résultats (titre, image, URL du flux RSS) prêts à être ajoutés.
## Périmètre & hors-périmètre
- Inclus : domaine `Podcast`/`PodcastSearchResult`, interface de recherche, impl. HTTP iTunes Search, use case, mapping DTO.
- Exclus : ajout/persistance (3.2/3.3), UI parent finale (J5).
## Dépendances
Jalon 0.
## Conception
- **Domain** (`features/podcasts/domain/`) :
- `Podcast` (entité) : `id`, `title`, `feedUrl`, `artworkUrl?`, `author?`.
- `PodcastSearchRepository` (interface) : `Future<Result<List<Podcast>>> search(String query)`.
- `PodcastFailure` : `SearchFailure`, `InvalidFeedFailure`, `FeedUnreachableFailure`.
- **Application** : `SearchPodcastsUseCase` (trim/garde-fou requête vide → `Err` ou liste vide selon choix documenté).
- **Data** (`features/podcasts/data/`) :
- `ItunesPodcastSearchRepository` : appelle `https://itunes.apple.com/search?media=podcast&term=...`.
- DTO `ItunesResultDto` + mapper → `Podcast` (`feedUrl` = champ `feedUrl` d'iTunes).
- Client HTTP injecté (interface fine) pour testabilité ; erreurs réseau → `SearchFailure`.
## Plan TDD
1. **Red** : `search_podcasts_use_case_test.dart` — requête non vide délègue au repo ; requête vide → comportement défini (documenter : `Err(SearchFailure)` ou `Ok([])`).
2. **Green** : implémenter le use case.
3. **Red** : `itunes_podcast_search_repository_test.dart` — client HTTP mocké renvoyant un JSON iTunes d'exemple → liste de `Podcast` correctement mappée ; HTTP 500 / timeout → `SearchFailure`.
4. **Green** : implémenter repo + mapper.
5. **Refactor**.
## Definition of Done
- Tests use case + repository verts (HTTP mocké, fixture JSON iTunes).
- `tool/check.sh` passe ; étape 3.1 cochée dans `ROADMAP.md`.
## Risques / notes
- Les résultats iTunes peuvent être larges/inadaptés ; le filtrage fin relève du parent (il choisit). Ne pas sur-filtrer côté code en v1.
- Certains résultats n'ont pas de `feedUrl` : les écarter du résultat (un podcast sans flux n'est pas exploitable).
@@ -0,0 +1,41 @@
# 3.2 — Ajout par URL RSS
## Objectif
Permettre au parent de coller l'URL d'un flux RSS, d'en obtenir un aperçu (titre,
image, nombre d'épisodes) et de récupérer les épisodes mappés vers `Episode`.
## Périmètre & hors-périmètre
- Inclus : récupération + parsing RSS, validation de l'URL, aperçu du podcast, mapping items RSS → `Episode` (feature playback).
- Exclus : persistance (3.3), UI finale (J5).
## Dépendances
3.1 (entité `Podcast`, `PodcastFailure`), 2.1 (`Episode`).
## Conception
- **Domain** (`features/podcasts/domain/`) :
- Étendre `PodcastSearchRepository` ou ajouter `FeedRepository` :
- `Future<Result<Podcast>> fetchFeedPreview(Uri feedUrl)` (titre/image depuis le flux).
- `Future<Result<List<Episode>>> fetchEpisodes(Uri feedUrl)`.
- **Application** :
- `PreviewFeedUseCase` (valide l'URL : schéma http/https, non vide → sinon `InvalidFeedFailure`).
- `LoadEpisodesUseCase`.
- **Data** :
- `RssFeedRepository` via `dart_rss` (ou `webfeed`) + client HTTP injecté.
- Mapper RSS item → `Episode` : `title`, `audioUrl` = `enclosure.url`, `duration` (itunes:duration si présent), `artworkUrl`.
- Items sans enclosure audio → ignorés (pas une histoire jouable).
- Erreurs : flux illisible → `InvalidFeedFailure` ; réseau → `FeedUnreachableFailure`.
## Plan TDD
1. **Red** : `preview_feed_use_case_test.dart` — URL vide / non http → `Err(InvalidFeedFailure)` ; URL valide délègue au repo.
2. **Green** : implémenter validation + use case.
3. **Red** : `rss_feed_repository_test.dart` — fixture XML RSS d'exemple → `Podcast` (aperçu) et `List<Episode>` corrects ; item sans enclosure ignoré ; XML invalide → `InvalidFeedFailure` ; HTTP KO → `FeedUnreachableFailure`.
4. **Green** : implémenter repo + mapper.
5. **Refactor**.
## Definition of Done
- Tests verts avec fixtures RSS (cas nominal, item sans audio, XML invalide).
- `tool/check.sh` passe ; étape 3.2 cochée dans `ROADMAP.md`.
## Risques / notes
- Diversité des flux RSS podcast (champs optionnels, namespaces itunes). Prévoir des fixtures variées.
- Garder le mapping RSS→`Episode` ici (feature podcasts), conformément à la note de 2.1.
@@ -0,0 +1,44 @@
# 3.3 — Persistance des abonnements + CRUD
## Objectif
Stocker localement les podcasts auxquels le parent a souscrit, et offrir les use
cases pour les lister, ajouter et supprimer.
## Périmètre & hors-périmètre
- Inclus : table SQLite des abonnements, `SubscriptionRepository`, use cases CRUD, providers.
- Exclus : UI de gestion finale (J5) ; cache des épisodes (streaming, pas de cache v1).
## Dépendances
3.1, 3.2.
## Conception
- **Domain** (`features/podcasts/domain/`) :
- `Subscription` (entité) : `id`, `podcast` (`Podcast`), `addedAt`.
- `SubscriptionRepository` (interface) :
- `Future<Result<List<Subscription>>> all()`
- `Future<Result<Unit>> add(Podcast podcast)` (idempotent sur `feedUrl`)
- `Future<Result<Unit>> remove(String id)`
- `Stream<List<Subscription>> watch()`
- **Application** : `ListSubscriptionsUseCase`, `AddSubscriptionUseCase` (refuse un doublon de `feedUrl`), `RemoveSubscriptionUseCase`.
- **Data** (`features/podcasts/data/`) :
- `SqliteSubscriptionRepository` (`sqflite`/`drift`). Table `subscriptions(id, title, feed_url UNIQUE, artwork_url, author, added_at)`.
- DTO/mapper ligne ↔ `Subscription`.
- **DI** : provider du repository ; base ouverte via un provider initialisé au démarrage.
## Plan TDD
1. **Red** : `add_subscription_use_case_test.dart` — ajout d'un nouveau `feedUrl``Ok` ; doublon → `Err`/no-op documenté.
2. **Green** : implémenter le use case.
3. **Red** : `list/remove` use cases — délèguent correctement au repo mocké.
4. **Green** : implémenter.
5. **Red** : `sqlite_subscription_repository_test.dart` — sur une base SQLite en mémoire : add→all renvoie l'élément ; contrainte UNIQUE respectée ; remove supprime ; `watch` émet après modification.
6. **Green** : implémenter le repo + schéma.
7. **Refactor**.
## Definition of Done
- Tests use cases + repo (SQLite en mémoire) verts.
- Les abonnements persistent après redémarrage de l'app (vérif. manuelle).
- `tool/check.sh` passe ; étape 3.3 cochée dans `ROADMAP.md`.
## Risques / notes
- Migrations : prévoir un numéro de version de schéma dès le départ, même simple.
- Source unique de vérité de la liste = ce repository ; l'UI enfant (J4) lira les épisodes via les abonnements.
+26
View File
@@ -0,0 +1,26 @@
# Jalon 3 — Découverte & gestion des podcasts
## Objectif
Permettre au parent de trouver des sources (recherche annuaire **et** URL RSS),
de les enregistrer, et d'en lister les épisodes (= les histoires).
## Périmètre
- Recherche via l'API iTunes Search (gratuite, sans clé).
- Ajout par collage d'URL RSS (parsing + aperçu).
- Persistance locale des abonnements (SQLite).
- Récupération des épisodes d'un podcast abonné (mapping RSS → `Episode` de J2).
## Hors-périmètre
- L'écran de gestion sera **abrité derrière le code parental en J5** ; ici, un accès temporaire (dev) suffit.
- Pas de cache offline des épisodes (streaming).
## Étapes
1. [3.1 — Recherche annuaire (iTunes Search)](01-recherche-itunes.md)
2. [3.2 — Ajout par URL RSS](02-ajout-rss.md)
3. [3.3 — Persistance des abonnements + CRUD](03-persistance-abonnements.md)
## Definition of Done (jalon)
- Le parent peut chercher un podcast, ou coller une URL RSS, et l'ajouter.
- Les abonnements survivent au redémarrage de l'app (SQLite).
- La liste des épisodes d'un abonnement est récupérable et mappée vers `Episode`.
- `tool/check.sh` passe ; `ROADMAP.md` 3.1→3.3 cochées.
@@ -0,0 +1,36 @@
# 4.1 — Liste des histoires
## Objectif
Afficher les histoires disponibles (épisodes des podcasts abonnés) sous forme de
liste de titres cliquables, et lancer la lecture au tap.
## Périmètre & hors-périmètre
- Inclus : agrégation des épisodes des abonnements, contrôleur Riverpod, écran liste, tap → `PlayStoryUseCase`.
- Exclus : verrouillage (4.2), limites (J6).
## Dépendances
2.x (lecture + `Episode`), 3.x (abonnements + `fetchEpisodes`).
## Conception
- **Application** (`features/podcasts/application/` ou écran enfant) :
- `LoadChildLibraryUseCase` : pour chaque abonnement, récupère les épisodes (`LoadEpisodesUseCase`) et construit la liste affichable. Stratégie d'erreur par flux : un flux KO n'empêche pas d'afficher les autres (dégradation gracieuse).
- **Presentation** (`features/.../presentation/`) :
- `ChildLibraryController` (Riverpod `AsyncNotifier`) : charge la bibliothèque, expose `loading/data/error`.
- `ChildLibraryView` : liste de cartes (vignette + titre, grande zone tactile). Regroupement par podcast optionnel (titre de section).
- Tap sur un épisode → `PlayStoryUseCase(queue, startIndex)` puis navigation vers/ouverture du `PlayerView` (J2).
- États vides/erreur : messages **enfant** rassurants (« Aucune histoire pour l'instant », « Oups, réessaie »), jamais d'erreur technique.
## Plan TDD
1. **Red** : `child_library_controller_test.dart` — avec use cases mockés : agrège les épisodes de 2 abonnements ; un abonnement en `Err` est ignoré sans faire échouer l'ensemble ; expose l'état attendu.
2. **Green** : implémenter le use case d'agrégation + contrôleur.
3. **Red** : `child_library_view_test.dart` (widget) — affiche les titres ; tap sur un titre appelle `PlayStoryUseCase` (provider mocké) avec le bon index ; état vide affiche le message enfant.
4. **Green** : implémenter le widget.
5. **Refactor**.
## Definition of Done
- Tests contrôleur + widget verts (dont dégradation gracieuse).
- Démo : la liste s'affiche, un tap lit l'histoire.
- `tool/check.sh` passe ; étape 4.1 cochée dans `ROADMAP.md`.
## Risques / notes
- Récupérer les épisodes de plusieurs flux peut être lent : charger en parallèle, afficher au fur et à mesure si pertinent. Rester simple en v1.
@@ -0,0 +1,37 @@
# 4.2 — Navigation verrouillée
## Objectif
Faire en sorte que l'espace enfant s'épingle automatiquement et qu'aucune action
de l'interface ne permette de sortir de l'app ou d'atteindre le système.
## Périmètre & hors-périmètre
- Inclus : déclenchement auto de l'épinglage à l'entrée de l'espace enfant, audit des sorties UI possibles, gestion du bouton retour.
- Exclus : la porte parentale (J5) ; les limites (J6).
## Dépendances
1.2 (use cases de verrouillage), 4.1 (espace enfant existant).
## Conception
- À l'entrée de l'espace enfant : `StartLockUseCase` appelé (via le contrôleur ou un observateur de cycle de vie). Gérer `LockUnsupportedFailure` → message parent au moment de la config, pas de crash.
- **Audit des échappatoires UI** :
- Pas de lien hypertexte ouvrant un navigateur, pas d'`intent` externe, pas de partage.
- Bouton retour Android : intercepté (`PopScope`/`WillPopScope`) pour ne pas quitter l'app.
- L'icône ⚙️ d'accès parent (posée ici) mène à la **porte parentale** (J5) ; sans le code, on ne sort pas de l'app.
- Réafficher/relancer l'épinglage si l'app revient au premier plan (cycle de vie `resumed`).
- **Honnêteté UI** : ne pas afficher de message laissant croire à un verrouillage inviolable.
## Plan TDD
1. **Red** : `child_shell_locking_test.dart` (widget/contrôleur) — à l'affichage de l'espace enfant, `StartLockUseCase` est appelé (provider mocké). En `unsupported`, pas de crash et état dégradé documenté.
2. **Green** : brancher l'appel d'épinglage au cycle de vie de l'écran.
3. **Red** : `child_shell_back_button_test.dart` — le `PopScope` empêche la fermeture (callback de pop non déclenché / app non quittée).
4. **Green** : implémenter l'interception.
5. **Validation manuelle** : sur appareil, parcourir l'écran enfant et tenter toutes les sorties (retour, accueil, multitâche) → bloquées ; consigner.
6. **Refactor**.
## Definition of Done
- Tests d'épinglage auto + interception retour verts.
- Audit manuel des sorties UI réalisé sur appareil, résultats consignés.
- `tool/check.sh` passe ; étape 4.2 cochée dans `ROADMAP.md`.
## Risques / notes
- Le re-épinglage au `resumed` doit éviter les boucles/flickers : tester le comportement de reprise.
@@ -0,0 +1,26 @@
# Jalon 4 — Espace enfant
## Objectif
Assembler l'écran que l'enfant utilise : liste des histoires disponibles (titres
cliquables) + lecteur, le tout dans une coquille **verrouillée** (épinglage auto,
aucune échappatoire via l'UI).
## Périmètre
- Liste des épisodes de tous les abonnements (ou par podcast), avec titre + vignette.
- Sélection d'un titre → lecture (réutilise le lecteur de J2).
- Épinglage déclenché automatiquement à l'entrée dans l'espace enfant (J1).
- Aucune action UI ne permet de quitter l'app ou d'accéder au système.
## Hors-périmètre
- Limites/avertissements (J6).
- L'accès à l'espace parent (icône) est posé ici mais la **porte** (code) vient en J5 ; en attendant, accès dev temporaire.
## Étapes
1. [4.1 — Liste des histoires](01-liste-histoires.md)
2. [4.2 — Navigation verrouillée](02-navigation-verrouillee.md)
## Definition of Done (jalon)
- L'enfant voit la liste, tape un titre, l'histoire se lit.
- À l'ouverture de l'espace enfant, l'app s'épingle automatiquement.
- Aucun bouton/geste UI ne sort de l'app (hors mécanisme système + PIN).
- `tool/check.sh` passe ; `ROADMAP.md` 4.1→4.2 cochées.
@@ -0,0 +1,39 @@
# 5.1 — Création du code au premier lancement
## Objectif
Au tout premier lancement, obliger le parent à créer un code à 4 chiffres (saisi
deux fois pour confirmation) avant d'accéder à l'app.
## Périmètre & hors-périmètre
- Inclus : détection « pas de code défini », écran de création (double saisie + validation), enregistrement.
- Exclus : stockage technique détaillé (5.2), porte d'accès (5.3).
## Dépendances
Jalon 0. (Le stockage de 5.2 peut être développé en parallèle/avant ; ici on consomme son interface.)
## Conception
- **Domain** (`features/parental/domain/`) :
- `ParentalCode` (value object) : exactement 4 chiffres ; invariant validé à la construction (sinon `Err(InvalidCodeFormat)`).
- `ParentalCodeRepository` (interface) : `Future<bool> isConfigured()`, `Future<Result<Unit>> setCode(ParentalCode code)`, `Future<bool> verify(ParentalCode code)`. (Impl. en 5.2.)
- **Application** :
- `IsCodeConfiguredUseCase`, `SetParentalCodeUseCase` (refuse si déjà configuré, ou autorise via flux dédié — documenter).
- **Presentation** :
- Au démarrage, `IsCodeConfiguredUseCase` décide : non configuré → `CreateCodeView` bloquant ; configuré → espace enfant.
- `CreateCodeView` : saisie 1 + saisie 2, contrôle d'égalité, format 4 chiffres, gros pavé numérique adapté.
## Plan TDD
1. **Red** : `parental_code_test.dart``ParentalCode('1234')` OK ; `'12'`, `'12a4'`, `''` → invalides.
2. **Green** : implémenter le value object.
3. **Red** : `set_parental_code_use_case_test.dart` — délègue à `setCode` ; refuse si déjà configuré (selon règle documentée).
4. **Green** : implémenter.
5. **Red** : `create_code_view_test.dart` (widget) — deux saisies différentes → erreur affichée, pas d'enregistrement ; deux saisies égales valides → `SetParentalCodeUseCase` appelé.
6. **Green** : implémenter l'écran.
7. **Refactor**.
## Definition of Done
- Tests value object + use case + widget verts.
- Premier lancement impose la création ; lancement ultérieur ne la redemande pas.
- `tool/check.sh` passe ; étape 5.1 cochée dans `ROADMAP.md`.
## Risques / notes
- Ne jamais logguer le code. Le value object ne doit pas exposer la valeur en clair dans `toString`.
@@ -0,0 +1,36 @@
# 5.2 — Stockage sécurisé & vérification
## Objectif
Implémenter `ParentalCodeRepository` : stocker le code de façon sûre (haché, jamais
en clair) et vérifier une saisie.
## Périmètre & hors-périmètre
- Inclus : hachage du code, stockage via `flutter_secure_storage`, vérification.
- Exclus : écrans (5.1/5.3).
## Dépendances
5.1 (interface + `ParentalCode`).
## Conception
- **Data** (`features/parental/data/`) :
- `SecureParentalCodeRepository implements ParentalCodeRepository`.
- Stocke un **hachage** du code (+ sel) dans `flutter_secure_storage` — jamais la valeur en clair.
- Pour 4 chiffres, l'espace est petit (10 000 combinaisons) : le hachage protège contre la lecture directe du stockage, sans prétendre à une résistance forte au brute-force hors-ligne. C'est cohérent avec le modèle de menace (garde-fou enfant, cf. CLAUDE.md §1). Documenter ce choix.
- `isConfigured()` = présence de la clé ; `verify(code)` = comparaison des hachages (comparaison à temps constant si simple à faire).
- **DI** : `parentalCodeRepositoryProvider`.
## Plan TDD
1. **Red** : `secure_parental_code_repository_test.dart` — avec un `flutter_secure_storage` mocké :
- `setCode` écrit une valeur **différente** du code en clair (hachée).
- `verify` renvoie `true` pour le bon code, `false` sinon.
- `isConfigured` reflète la présence de la clé.
2. **Green** : implémenter hachage + repository.
3. **Refactor**.
## Definition of Done
- Tests verts ; aucune écriture du code en clair (vérifié par le test).
- `tool/check.sh` passe ; étape 5.2 cochée dans `ROADMAP.md` ; choix du modèle de menace consigné.
## Risques / notes
- `flutter_secure_storage` s'appuie sur le Keystore Android : tester aussi le cas « valeur absente » au premier lancement.
- Ne pas réinventer de crypto : utiliser une fonction de hachage standard de `crypto`.
@@ -0,0 +1,37 @@
# 5.3 — Accès à l'espace parent & abritage de la gestion podcasts
## Objectif
Relier l'icône discrète de l'écran enfant à une porte parentale (saisie du code)
qui ouvre l'espace parent, et y intégrer la gestion des abonnements (J3).
## Périmètre & hors-périmètre
- Inclus : porte parentale (saisie + vérification), espace parent, déplacement de la gestion podcasts (J3) derrière cette porte.
- Exclus : réglages de limites (J6, qui s'ajouteront à l'espace parent).
## Dépendances
5.1, 5.2 (code + vérification), 3.x (gestion podcasts), 4.2 (icône posée).
## Conception
- **Application** : `VerifyParentalCodeUseCase` (utilise `ParentalCodeRepository.verify`).
- **Presentation** (`features/parental/presentation/`) :
- `ParentGateView` : pavé numérique, saisie 4 chiffres → `VerifyParentalCodeUseCase`. Bon code → navigation espace parent ; mauvais → message + reset saisie. Anti-spam simple (léger délai après N échecs) optionnel, documenté.
- `ParentHomeView` : menu de l'espace parent → « Mes podcasts » (écrans de J3), « Réglages » (changer le code), placeholder « Limites » (rempli en J6).
- Brancher les écrans de gestion podcasts de J3 ici, et **retirer l'accès dev temporaire** mis en place au J3.
- Sortie de l'espace parent → retour à l'écran enfant (qui réactive l'épinglage, J4).
## Plan TDD
1. **Red** : `verify_parental_code_use_case_test.dart` — bon code → `true` ; mauvais → `false` (repo mocké).
2. **Green** : implémenter.
3. **Red** : `parent_gate_view_test.dart` (widget) — saisie correcte → navigation vers l'espace parent (provider mocké) ; incorrecte → message d'erreur, pas de navigation.
4. **Green** : implémenter la porte.
5. **Red** : `parent_home_view_test.dart` — les entrées de menu mènent aux écrans attendus (podcasts, réglages).
6. **Green** : implémenter l'espace parent + câbler la gestion podcasts.
7. **Refactor** : supprimer l'accès dev temporaire de J3 ; vérifier qu'on n'atteint plus la gestion podcasts sans code.
## Definition of Done
- Tests use case + porte + accueil parent verts.
- Depuis l'écran enfant : ⚙️ → code correct → gestion des podcasts ; code incorrect → refus ; aucun accès à la gestion sans code.
- `tool/check.sh` passe ; étape 5.3 cochée dans `ROADMAP.md`.
## Risques / notes
- Vérifier qu'aucune route résiduelle (dev J3) ne contourne la porte parentale.
@@ -0,0 +1,27 @@
# Jalon 5 — Code parental & espace parent
## Objectif
Mettre en place le code parental (création au premier lancement, vérification) et
l'espace parent protégé qui abrite la gestion des podcasts (J3) et, plus tard, les
limites (J6).
## Périmètre
- Création du code à 4 chiffres au premier lancement (double saisie).
- Stockage sécurisé du code (haché, jamais en clair).
- Porte parentale : icône discrète → saisie du code → espace parent.
- Espace parent abritant la gestion des abonnements (déplacée depuis l'accès dev de J3).
## Hors-périmètre
- Récupération de code oublié : **exclu v1** (réinstaller réinitialise tout — décision actée).
- Réglages de limites : posés en J6 (l'espace parent les accueillera).
## Étapes
1. [5.1 — Création du code au premier lancement](01-creation-code.md)
2. [5.2 — Stockage sécurisé & vérification](02-stockage-securise.md)
3. [5.3 — Accès à l'espace parent & abritage gestion podcasts](03-acces-espace-parent.md)
## Definition of Done (jalon)
- Premier lancement → création du code obligatoire ; relances suivantes → pas redemandé.
- Code stocké haché ; vérification correcte/incorrecte gérée.
- L'icône ⚙️ ouvre la porte parentale ; bon code → espace parent (gestion podcasts) ; mauvais code → refus.
- `tool/check.sh` passe ; `ROADMAP.md` 5.1→5.3 cochées.
@@ -0,0 +1,43 @@
# 6.1 — Domaine des limites
## Objectif
Modéliser les limites de coucher (minuterie + nombre d'histoires) et la logique
qui décide quand avertir et quand arrêter, indépendamment de l'UI.
## Périmètre & hors-périmètre
- Inclus : configuration des limites, compteurs, calcul des seuils d'avertissement et d'arrêt, reset quotidien, persistance des réglages + compteurs.
- Exclus : affichage des avertissements (6.2), écran de fin (6.3).
## Dépendances
2.x (événements de lecture : histoire démarrée, position/temps), Jalon 0.
## Conception
- **Domain** (`features/limits/domain/`) :
- `LimitSettings` (value object) : `timerEnabled`, `timerDuration?`, `storyCountEnabled`, `maxStories?`.
- `SessionCounters` : `elapsed` (Duration), `storiesPlayed` (int), `day` (date du compteur, pour le reset).
- `LimitStatus` (résultat de décision) : `ok`, `warnTimeSoon`, `warnLastStory`, `reached`.
- `LimitEvaluator` (service de domaine pur) : `LimitStatus evaluate(LimitSettings, SessionCounters)` — applique les seuils (ex. avertissement minuterie à T-5 min ; « dernière histoire » quand `storiesPlayed == maxStories - 1`).
- `LimitsRepository` (interface) : `get/saveSettings`, `get/saveCounters`, reset si le jour a changé.
- **Application** :
- `GetLimitSettingsUseCase`, `SaveLimitSettingsUseCase`.
- `RegisterStoryStartedUseCase` (incrémente le compteur + reset si nouveau jour).
- `TickElapsedUseCase` / `EvaluateLimitsUseCase` (renvoie `LimitStatus`).
- **Data** : `PrefsLimitsRepository` via `shared_preferences` (réglages + compteurs + date du jour). Reset : si la date stockée ≠ aujourd'hui, compteurs remis à zéro à la lecture.
## Plan TDD
1. **Red** : `limit_evaluator_test.dart` — table de cas : minuterie désactivée → toujours `ok` jusqu'au compteur d'histoires ; T-5 min → `warnTimeSoon` ; temps atteint → `reached` ; `storiesPlayed == max-1``warnLastStory` ; `== max``reached`. Les deux limites combinées : la plus restrictive gagne.
2. **Green** : implémenter `LimitEvaluator` (logique pure).
3. **Red** : `register_story_started_use_case_test.dart` — incrémente ; si nouveau jour, repart de 0.
4. **Green** : implémenter.
5. **Red** : `prefs_limits_repository_test.dart` — sauvegarde/relecture des réglages et compteurs ; reset au changement de date (date injectée, pas `DateTime.now()` direct → horloge injectable).
6. **Green** : implémenter le repo + horloge injectable.
7. **Refactor**.
## Definition of Done
- Tests évaluateur (table de cas) + use cases + repo verts.
- Logique de décision sans dépendance Flutter.
- `tool/check.sh` passe ; étape 6.1 cochée dans `ROADMAP.md`.
## Risques / notes
- Injecter une `Clock` (interface) pour tester le reset quotidien sans dépendre de l'heure réelle.
- Le seuil d'avertissement minuterie (5 min) est une constante nommée, ajustable.
@@ -0,0 +1,36 @@
# 6.2 — Avertissements doux
## Objectif
Prévenir l'enfant en douceur, **en amont** de l'échéance, sans interrompre l'écoute :
« Encore 5 minutes 🌙 », « C'est la dernière histoire ⭐ ».
## Périmètre & hors-périmètre
- Inclus : observation de `LimitStatus`, affichage non bloquant des avertissements, anti-répétition.
- Exclus : l'arrêt et l'écran de fin (6.3).
## Dépendances
6.1 (`LimitEvaluator`, `LimitStatus`), 2.x (lecture en cours), 4.x (espace enfant).
## Conception
- **Presentation** (`features/limits/presentation/`) :
- `LimitsController` (Riverpod) : combine les compteurs (temps via un tick périodique, histoires via les événements de lecture) et appelle `EvaluateLimitsUseCase` → expose `LimitStatus`.
- Sur transition vers `warnTimeSoon` / `warnLastStory` : afficher un overlay/snackbar doux (texte + emoji), **non bloquant**, qui se referme seul. La lecture continue.
- **Anti-répétition** : chaque avertissement ne s'affiche qu'une fois par session/seuil (mémoriser le dernier `LimitStatus` notifié).
- Ton et style : doux, rassurant, cohérent avec le thème coucher (0.3).
- Le tick de temps : un timer léger qui met à jour `elapsed` et réévalue ; s'arrête quand la lecture est en pause.
## Plan TDD
1. **Red** : `limits_controller_test.dart` — séquence d'états simulés : passage à `warnTimeSoon` déclenche **une** notification ; rester en `warnTimeSoon` ne la re-déclenche pas ; `warnLastStory` notifie une fois.
2. **Green** : implémenter le contrôleur + anti-répétition.
3. **Red** : `warning_overlay_test.dart` (widget) — sur `warnTimeSoon`, le texte « Encore 5 minutes » apparaît puis disparaît ; la lecture n'est pas interrompue (le lecteur reste monté/actif).
4. **Green** : implémenter l'overlay.
5. **Refactor**.
## Definition of Done
- Tests contrôleur (anti-répétition) + widget verts.
- Démo : les avertissements apparaissent au bon moment, une seule fois, sans couper le son.
- `tool/check.sh` passe ; étape 6.2 cochée dans `ROADMAP.md`.
## Risques / notes
- Le timer doit se mettre en pause avec la lecture pour ne pas avertir/arrêter à tort pendant une pause.
- Textes des messages = constantes nommées (faciles à ajuster / traduire plus tard).
@@ -0,0 +1,39 @@
# 6.3 — Écran de fin & reprise parent
## Objectif
À l'atteinte d'une limite, arrêter la lecture et afficher un écran apaisant
« C'est fini pour ce soir 🌙 », l'app restant épinglée ; seule la saisie du code
parental permet de reprendre.
## Périmètre & hors-périmètre
- Inclus : réaction à `LimitStatus.reached` (stop lecture + écran de fin), verrouillage de la reprise derrière le code parental.
- Exclus : la logique de décision (6.1) et les avertissements (6.2).
## Dépendances
6.1, 6.2, 2.x (arrêt lecture), 1.x (épinglage maintenu), 5.x (vérification du code).
## Conception
- **Presentation** :
- Quand `LimitsController` passe à `reached` : appeler l'arrêt de lecture (`PauseStoryUseCase`/stop) puis afficher `BedtimeOverEndView` (plein écran, doux, étoiles/lune).
- L'app **reste épinglée** ; l'écran de fin ne propose aucune sortie système.
- **Reprise** : un bouton discret « Parent » → `ParentGateView` (J5). Code correct → réinitialise/lève la limite pour la session (ou ouvre l'espace parent pour ajuster les réglages) ; code incorrect → reste sur l'écran de fin.
- Décision à figer : reprendre annule-t-il la limite du soir, ou ré-autorise-t-il une histoire de plus ? → documenter le comportement choisi.
- Cohérence avec le reset quotidien (6.1) : le lendemain, compteurs repartis de zéro.
## Plan TDD
1. **Red** : `end_session_flow_test.dart` (contrôleur) — transition vers `reached` → appel d'arrêt de lecture + signal d'affichage de l'écran de fin.
2. **Green** : implémenter la réaction.
3. **Red** : `bedtime_over_end_view_test.dart` (widget) — l'écran de fin s'affiche ; le bouton « Parent » ouvre la porte ; code correct → reprise selon comportement documenté ; incorrect → reste bloqué.
4. **Green** : implémenter l'écran + câblage avec la porte parentale.
5. **Validation manuelle** : sur appareil, atteindre une limite (régler un court délai) → lecture stoppée, écran de fin, reprise uniquement par code, app non quittable.
6. **Refactor**.
## Definition of Done
- Tests flux de fin + écran + reprise verts.
- Démo manuelle complète consignée (limite atteinte → fin → reprise parent).
- Comportement de reprise documenté.
- `tool/check.sh` passe ; étape 6.3 cochée dans `ROADMAP.md`.
## Risques / notes
- S'assurer que l'écran de fin ne laisse aucune faille de navigation vers le système.
- Bien gérer le cas « les deux limites » : la première atteinte déclenche la fin.
+27
View File
@@ -0,0 +1,27 @@
# Jalon 6 — Limites, avertissements & fin de session
## Objectif
Permettre au parent de fixer des limites de coucher (minuterie **et** nombre
d'histoires), prévenir l'enfant en douceur avant l'échéance, puis arrêter la
lecture sur un écran apaisant que seul le parent peut lever.
## Périmètre
- Réglages parent : activer/désactiver minuterie (X min) et nombre d'histoires (X), indépendamment.
- Compteurs : temps écoulé + histoires jouées, **reset automatique chaque jour**.
- Avertissements doux **en amont** : « Encore 5 minutes 🌙 », « C'est la dernière histoire ⭐ ».
- À l'atteinte : arrêt de la lecture + écran « C'est fini pour ce soir 🌙 », app toujours épinglée, reprise uniquement par le parent (code).
## Hors-périmètre
- Planning par jour de la semaine, horaires : exclu v1 (limites simples).
## Étapes
1. [6.1 — Domaine des limites](01-domaine-limites.md)
2. [6.2 — Avertissements doux](02-avertissements.md)
3. [6.3 — Écran de fin & reprise parent](03-fin-session.md)
## Definition of Done (jalon)
- Le parent configure les deux types de limites depuis l'espace parent (J5).
- Les avertissements s'affichent en amont, sans interrompre.
- À l'échéance, la lecture s'arrête, l'écran de fin s'affiche, et seul le code parental permet de reprendre.
- Les compteurs se remettent à zéro chaque jour.
- `tool/check.sh` passe ; `ROADMAP.md` 6.1→6.3 cochées.