commit 16fd4c8c3669a886d2fdf63a8e2adc92cdd83927 Author: Vincent Bourdon Date: Fri Jun 19 17:03:33 2026 +0200 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca317b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ / Android Studio +*.iml +*.ipr +*.iws +.idea/ + +# VS Code +.vscode/ + +# Flutter/Dart/Pub +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds +# NB : pubspec.lock est volontairement versionné (projet applicatif, build reproductible) + +# Symbolication / Obfuscation +app.*.symbols +app.*.map.json + +# Android +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks +*.keystore + +# Local & secrets +*.env +.env* +secrets.dart + +# Project tooling +.omc/ +/tmp/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4fb34d5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,150 @@ +# Storytime — Guide projet & principes d'ingénierie + +> Lecteur d'histoires audio cadenassé pour le coucher, sur tablette Android. +> L'enfant écoute des histoires (podcasts) sans pouvoir sortir de l'application ; +> le parent gère les sources, le code parental et les limites. + +Ce fichier est le contrat d'ingénierie du projet. **Toute contribution doit le respecter.** + +--- + +## 1. Le produit en une page + +- **Plateforme** : Android uniquement. **Flutter / Dart**. +- **Lecture** : streaming (pas de hors-ligne en v1). +- **Deux mondes** : + - **Espace enfant** (par défaut, verrouillé) : liste de titres d'histoires, gros boutons lecture/pause/suivant. + - **Espace parent** (protégé par code 4 chiffres) : gestion des podcasts, limites, réglages. +- **Verrouillage** : Screen Pinning (épinglage natif Android). + +### Contraintes connues (à ne jamais oublier) +- L'épinglage empêche la **sortie facile**, mais la sortie réelle repose sur le **mécanisme natif Android + le PIN de la tablette**, PAS sur notre code parental. C'est un garde-fou enfant, pas un coffre-fort. Ne jamais promettre plus dans l'UI ou les messages. +- L'épinglage **ne survit pas à un redémarrage** de la tablette. +- Le code parental protège **l'accès aux écrans parent** et la **reprise après limite**, pas le désépinglage système. + +--- + +## 2. Philosophie de développement + +Le projet suit le **Software Craftsmanship** : code soigné, testé, simple, qui se lit comme une explication du domaine. + +### TDD — non négociable +Cycle **Red → Green → Refactor** pour toute fonctionnalité ou correction : +1. **Red** : écrire un test qui décrit le comportement attendu. Le lancer, vérifier qu'il échoue **pour la bonne raison**. +2. **Green** : écrire le minimum de code pour le faire passer. +3. **Refactor** : nettoyer sans changer le comportement, tests toujours verts. + +Ne jamais écrire de code de production sans test qui le justifie. Les seules exceptions : code purement déclaratif (constantes, thèmes) et glue framework triviale — et même là, le comportement observable se teste. + +### Clean Code +- Noms qui révèlent l'intention ; pas d'abréviations obscures. +- Fonctions courtes, une seule raison d'exister. +- Pas de commentaire qui paraphrase le code ; un commentaire explique le **pourquoi**, jamais le **quoi**. +- Pas de nombre/chaîne magique : constantes nommées. +- Gestion d'erreur explicite (voir `Result`/`Either`, §4). +- **Boy Scout Rule** : laisser le code un peu plus propre qu'on ne l'a trouvé, sans dérive hors-sujet. + +### YAGNI +On n'implémente que ce que le jalon courant exige. Pas d'abstraction « au cas où ». + +--- + +## 3. Clean Architecture + +Dépendances dirigées **vers le domaine**. Le domaine ne connaît ni Flutter, ni HTTP, ni la base de données. + +``` +Presentation ─────► Application (use cases) ─────► Domain ◄───── Data (infra) + (UI/Riverpod) (orchestration) (cœur métier) (impl. dépôts) +``` + +### Couches +- **Domain** (`domain/`) — entités, value objects, interfaces de dépôts (abstraites), erreurs métier. **Zéro dépendance externe.** Dart pur. +- **Application** (`application/`) — *use cases* : une classe = une intention métier (`PlayStoryUseCase`, `AddPodcastUseCase`). Orchestrent le domaine via les interfaces de dépôts. Dart pur. +- **Data** (`data/`) — implémentations concrètes des dépôts, *data sources* (HTTP iTunes, parsing RSS, SQLite, secure storage), DTO/mappers. Dépend du domaine, jamais l'inverse. +- **Presentation** (`presentation/`) — écrans, widgets, contrôleurs d'état (Riverpod Notifiers). Appelle les use cases. Ne contient pas de logique métier. + +### Règle de dépendance +Une couche ne dépend que de couches plus internes. **Le domaine n'importe jamais `package:flutter`, `package:http`, `sqflite`, etc.** Toute violation est un bug d'architecture. + +### Injection de dépendances +**Riverpod** comme conteneur DI + gestion d'état. Les use cases et dépôts sont fournis via des `Provider`. En test, on *override* les providers par des doubles. + +--- + +## 4. Conventions de code + +- **État/DI** : Riverpod (`Notifier`/`AsyncNotifier`). +- **Immutabilité** : modèles immuables (`freezed` ou `copyWith` manuel pour les entités simples). +- **Erreurs** : pas d'exception qui traverse les couches en silence. Le domaine renvoie un type `Result` (ou `Either`). Les `Failure` sont des types métier (`NetworkFailure`, `InvalidFeedFailure`, …). Les exceptions techniques sont capturées dans la couche `data` et converties en `Failure`. +- **Asynchrone** : `Future`/`Stream` typés ; pas de `dynamic`. +- **Lint** : `flutter_lints` + règles strictes activées (`analysis_options.yaml`). Le projet doit rester **0 warning**. +- **Format** : `dart format` ; pas de débat de style. + +### Arborescence cible +``` +lib/ + core/ # transverse : Result, erreurs de base, constantes, thème, router + features/ + locking/ # verrouillage / épinglage + domain/ + application/ + data/ + presentation/ + playback/ # lecture audio + podcasts/ # recherche, RSS, abonnements + parental/ # code parental, accès espace parent + limits/ # minuterie, compteur d'histoires, avertissements + main.dart +test/ # miroir de lib/ ; tests unitaires & use cases +test/widget/ # tests de widgets +integration_test/ # tests d'intégration (épinglage, parcours bout-en-bout) +``` + +Feature-first, puis découpage en couches dans chaque feature. **Un fichier = une responsabilité.** Quand un fichier grossit, c'est le signal qu'il fait trop de choses : on le scinde. + +--- + +## 5. Stratégie de test + +| Niveau | Cible | Outils | +|--------|-------|--------| +| Unitaire | Domain + use cases (la majorité des tests) | `flutter_test`, `mocktail` | +| Widget | Écrans/widgets isolés | `flutter_test` | +| Intégration | Épinglage réel, parcours enfant/parent | `integration_test` sur appareil/émulateur | + +- Les tests de domaine/use cases ne touchent **jamais** au réseau, au disque ni à Flutter. +- Les dépendances externes (HTTP, RSS, stockage, plugin d'épinglage) sont derrière des interfaces, *mockées* en test. +- **Le jalon 1 (épinglage) exige une validation sur appareil réel** : un test/POC manuel documenté en plus des tests automatisés. + +--- + +## 6. Processus & suivi + +- **`docs/specs/`** : la spec, découpée par **jalon** (un dossier par jalon). Chaque jalon a un `README.md` (objectif + DoD) et ses **étapes en sous-fichiers numérotés**. +- **`ROADMAP.md`** : vue d'ensemble de l'avancement. **À mettre à jour à la fin de chaque étape** (cocher la case, dater, noter les écarts). C'est la source de vérité du « où on en est ». +- On traite **un jalon à la fois**, **une étape à la fois**, en TDD. +- **Definition of Done** d'une étape : code + tests verts + 0 warning lint + `ROADMAP.md` à jour. + +### Rituel à chaque étape terminée +1. Tous les tests de l'étape passent. +2. `flutter analyze` ne renvoie aucun warning. +3. Cocher l'étape dans `ROADMAP.md` (avec la date du jour). +4. Si un écart avec la spec est apparu, le noter dans le fichier d'étape concerné. + +--- + +## 7. Décisions techniques figées + +| Sujet | Choix | Raison | +|-------|-------|--------| +| Langage/UI | Flutter / Material 3 | Mono-plateforme Android, connu de l'auteur | +| État + DI | Riverpod | Testable, override facile en test | +| Audio | `just_audio` + `audio_service` | Lecture + contrôle arrière-plan | +| Épinglage | `kiosk_mode` (plugin), fallback platform channel Kotlin | API native Screen Pinning | +| Recherche podcasts | API iTunes Search (gratuite, sans clé) | Annuaire public | +| RSS | `dart_rss` (ou `webfeed`) | Parsing de flux | +| Persistance | `sqflite`/`drift` (abonnements) + `flutter_secure_storage` (code haché) | Local, sûr | +| Lecture seule réglages | `shared_preferences` | Compteurs/limites | + +Tout changement de cette table doit être justifié dans le commit et reflété ici. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..7c1d374 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,82 @@ +# Storytime — Roadmap + +> Vue d'ensemble de l'avancement. **À tenir à jour à la fin de chaque étape** +> (cocher la case + dater au format AAAA-MM-JJ). Source de vérité du « où on en est ». +> +> Légende : `[ ]` à faire · `[~]` en cours · `[x]` terminé (date) + +Spec détaillée : [`docs/specs/`](docs/specs/) — un dossier par jalon, étapes en sous-fichiers. + +--- + +## État global + +| Jalon | Titre | Statut | Bloquant | +|-------|-------|--------|----------| +| 0 | Fondations | `[ ]` à faire | — | +| 1 | Verrouillage / épinglage | `[ ]` à faire | ⚠️ **bloquant projet** | +| 2 | Lecture audio | `[ ]` à faire | — | +| 3 | Découverte & gestion des podcasts | `[ ]` à faire | — | +| 4 | Espace enfant | `[ ]` à faire | — | +| 5 | Code parental & espace parent | `[ ]` à faire | — | +| 6 | Limites, avertissements & fin de session | `[ ]` à faire | — | + +> ⚠️ Le jalon 1 valide la faisabilité de l'exigence n°1 (interdire la sortie). +> Si l'épinglage ne tient pas (plugin + fallback natif), on reconsidère la stratégie matérielle (tablette dédiée) **avant** d'investir dans le reste. + +--- + +## Jalon 0 — Fondations +Spec : [`docs/specs/jalon-0-fondations/`](docs/specs/jalon-0-fondations/) + +- [ ] 0.1 — Structure du projet Flutter & arborescence clean archi +- [ ] 0.2 — Outillage qualité (lint strict, format, CI locale) +- [ ] 0.3 — Socle transverse (`Result`, erreurs, thème, router, DI Riverpod) + +## Jalon 1 — Verrouillage / épinglage ⚠️ +Spec : [`docs/specs/jalon-1-verrouillage/`](docs/specs/jalon-1-verrouillage/) + +- [ ] 1.1 — Spike `kiosk_mode` sur appareil réel (épingler/désépingler) — **bloquant** +- [ ] 1.2 — Service de verrouillage (interface domaine + impl. + use cases) +- [ ] 1.3 — Fallback natif Kotlin via platform channel (si 1.1 insuffisant) + +## Jalon 2 — Lecture audio +Spec : [`docs/specs/jalon-2-lecture-audio/`](docs/specs/jalon-2-lecture-audio/) + +- [ ] 2.1 — Domaine de lecture (entités épisode/histoire, état de lecture) +- [ ] 2.2 — Service audio (`just_audio` + `audio_service`) derrière interface +- [ ] 2.3 — UI lecteur (boutons, progression) + contrôleur Riverpod + +## Jalon 3 — Découverte & gestion des podcasts +Spec : [`docs/specs/jalon-3-podcasts/`](docs/specs/jalon-3-podcasts/) + +- [ ] 3.1 — Recherche annuaire (API iTunes Search) +- [ ] 3.2 — Ajout par URL RSS (parsing + aperçu) +- [ ] 3.3 — Persistance des abonnements (SQLite) + use cases CRUD + +## Jalon 4 — Espace enfant +Spec : [`docs/specs/jalon-4-espace-enfant/`](docs/specs/jalon-4-espace-enfant/) + +- [ ] 4.1 — Liste des histoires (titres cliquables, vignettes) +- [ ] 4.2 — Navigation verrouillée (épinglage auto au démarrage, pas d'échappatoire UI) + +## Jalon 5 — Code parental & espace parent +Spec : [`docs/specs/jalon-5-code-parental/`](docs/specs/jalon-5-code-parental/) + +- [ ] 5.1 — Création du code au premier lancement (4 chiffres, double saisie) +- [ ] 5.2 — Stockage sécurisé (haché) + vérification du code +- [ ] 5.3 — Accès à l'espace parent (icône discrète → saisie code) + abritage de la gestion podcasts + +## Jalon 6 — Limites, avertissements & fin de session +Spec : [`docs/specs/jalon-6-limites/`](docs/specs/jalon-6-limites/) + +- [ ] 6.1 — Domaine des limites (minuterie + compteur d'histoires, reset quotidien) +- [ ] 6.2 — Avertissements doux en amont (« encore 5 min », « dernière histoire ») +- [ ] 6.3 — Écran de fin « C'est fini pour ce soir 🌙 » + reprise parent uniquement + +--- + +## Journal des décisions / écarts +> Noter ici toute décision prise en cours de route ou tout écart avec la spec. + +- _(vide pour l'instant)_ diff --git a/docs/specs/00-vision-architecture.md b/docs/specs/00-vision-architecture.md new file mode 100644 index 0000000..1e8e5b1 --- /dev/null +++ b/docs/specs/00-vision-architecture.md @@ -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. diff --git a/docs/specs/README.md b/docs/specs/README.md new file mode 100644 index 0000000..cdba137 --- /dev/null +++ b/docs/specs/README.md @@ -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/) diff --git a/docs/specs/jalon-0-fondations/01-structure-projet.md b/docs/specs/jalon-0-fondations/01-structure-projet.md new file mode 100644 index 0000000..9499bfa --- /dev/null +++ b/docs/specs/jalon-0-fondations/01-structure-projet.md @@ -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é. diff --git a/docs/specs/jalon-0-fondations/02-outillage-qualite.md b/docs/specs/jalon-0-fondations/02-outillage-qualite.md new file mode 100644 index 0000000..06d0f33 --- /dev/null +++ b/docs/specs/jalon-0-fondations/02-outillage-qualite.md @@ -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. diff --git a/docs/specs/jalon-0-fondations/03-socle-transverse.md b/docs/specs/jalon-0-fondations/03-socle-transverse.md new file mode 100644 index 0000000..7026752 --- /dev/null +++ b/docs/specs/jalon-0-fondations/03-socle-transverse.md @@ -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` : type `sealed` `Ok(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. diff --git a/docs/specs/jalon-0-fondations/README.md b/docs/specs/jalon-0-fondations/README.md new file mode 100644 index 0000000..fd2aaa1 --- /dev/null +++ b/docs/specs/jalon-0-fondations/README.md @@ -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. diff --git a/docs/specs/jalon-1-verrouillage/01-spike-kiosk-mode.md b/docs/specs/jalon-1-verrouillage/01-spike-kiosk-mode.md new file mode 100644 index 0000000..f4c7164 --- /dev/null +++ b/docs/specs/jalon-1-verrouillage/01-spike-kiosk-mode.md @@ -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. diff --git a/docs/specs/jalon-1-verrouillage/02-service-verrouillage.md b/docs/specs/jalon-1-verrouillage/02-service-verrouillage.md new file mode 100644 index 0000000..d73f761 --- /dev/null +++ b/docs/specs/jalon-1-verrouillage/02-service-verrouillage.md @@ -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> startLock()` + - `Future> stopLock()` + - `Stream watchState()` + - `Future 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. diff --git a/docs/specs/jalon-1-verrouillage/03-fallback-natif-kotlin.md b/docs/specs/jalon-1-verrouillage/03-fallback-natif-kotlin.md new file mode 100644 index 0000000..ad3d517 --- /dev/null +++ b/docs/specs/jalon-1-verrouillage/03-fallback-natif-kotlin.md @@ -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. diff --git a/docs/specs/jalon-1-verrouillage/README.md b/docs/specs/jalon-1-verrouillage/README.md new file mode 100644 index 0000000..f203b50 --- /dev/null +++ b/docs/specs/jalon-1-verrouillage/README.md @@ -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). diff --git a/docs/specs/jalon-2-lecture-audio/01-domaine-lecture.md b/docs/specs/jalon-2-lecture-audio/01-domaine-lecture.md new file mode 100644 index 0000000..e1d398e --- /dev/null +++ b/docs/specs/jalon-2-lecture-audio/01-domaine-lecture.md @@ -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> load(List queue, {int startIndex})` + - `Future> play()` / `pause()` / `next()` / `previous()` + - `Future> seek(Duration position)` + - `Stream 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. diff --git a/docs/specs/jalon-2-lecture-audio/02-service-audio.md b/docs/specs/jalon-2-lecture-audio/02-service-audio.md new file mode 100644 index 0000000..2289612 --- /dev/null +++ b/docs/specs/jalon-2-lecture-audio/02-service-audio.md @@ -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. diff --git a/docs/specs/jalon-2-lecture-audio/03-ui-lecteur.md b/docs/specs/jalon-2-lecture-audio/03-ui-lecteur.md new file mode 100644 index 0000000..f208c5d --- /dev/null +++ b/docs/specs/jalon-2-lecture-audio/03-ui-lecteur.md @@ -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. diff --git a/docs/specs/jalon-2-lecture-audio/README.md b/docs/specs/jalon-2-lecture-audio/README.md new file mode 100644 index 0000000..4b1a1a7 --- /dev/null +++ b/docs/specs/jalon-2-lecture-audio/README.md @@ -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. diff --git a/docs/specs/jalon-3-podcasts/01-recherche-itunes.md b/docs/specs/jalon-3-podcasts/01-recherche-itunes.md new file mode 100644 index 0000000..f06fd2f --- /dev/null +++ b/docs/specs/jalon-3-podcasts/01-recherche-itunes.md @@ -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>> 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). diff --git a/docs/specs/jalon-3-podcasts/02-ajout-rss.md b/docs/specs/jalon-3-podcasts/02-ajout-rss.md new file mode 100644 index 0000000..d68dccc --- /dev/null +++ b/docs/specs/jalon-3-podcasts/02-ajout-rss.md @@ -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> fetchFeedPreview(Uri feedUrl)` (titre/image depuis le flux). + - `Future>> 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` 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. diff --git a/docs/specs/jalon-3-podcasts/03-persistance-abonnements.md b/docs/specs/jalon-3-podcasts/03-persistance-abonnements.md new file mode 100644 index 0000000..14e2866 --- /dev/null +++ b/docs/specs/jalon-3-podcasts/03-persistance-abonnements.md @@ -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>> all()` + - `Future> add(Podcast podcast)` (idempotent sur `feedUrl`) + - `Future> remove(String id)` + - `Stream> 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. diff --git a/docs/specs/jalon-3-podcasts/README.md b/docs/specs/jalon-3-podcasts/README.md new file mode 100644 index 0000000..4d0097c --- /dev/null +++ b/docs/specs/jalon-3-podcasts/README.md @@ -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. diff --git a/docs/specs/jalon-4-espace-enfant/01-liste-histoires.md b/docs/specs/jalon-4-espace-enfant/01-liste-histoires.md new file mode 100644 index 0000000..ff4341c --- /dev/null +++ b/docs/specs/jalon-4-espace-enfant/01-liste-histoires.md @@ -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. diff --git a/docs/specs/jalon-4-espace-enfant/02-navigation-verrouillee.md b/docs/specs/jalon-4-espace-enfant/02-navigation-verrouillee.md new file mode 100644 index 0000000..61bcbe6 --- /dev/null +++ b/docs/specs/jalon-4-espace-enfant/02-navigation-verrouillee.md @@ -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. diff --git a/docs/specs/jalon-4-espace-enfant/README.md b/docs/specs/jalon-4-espace-enfant/README.md new file mode 100644 index 0000000..7af484a --- /dev/null +++ b/docs/specs/jalon-4-espace-enfant/README.md @@ -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. diff --git a/docs/specs/jalon-5-code-parental/01-creation-code.md b/docs/specs/jalon-5-code-parental/01-creation-code.md new file mode 100644 index 0000000..3de8407 --- /dev/null +++ b/docs/specs/jalon-5-code-parental/01-creation-code.md @@ -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 isConfigured()`, `Future> setCode(ParentalCode code)`, `Future 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`. diff --git a/docs/specs/jalon-5-code-parental/02-stockage-securise.md b/docs/specs/jalon-5-code-parental/02-stockage-securise.md new file mode 100644 index 0000000..3a6e28a --- /dev/null +++ b/docs/specs/jalon-5-code-parental/02-stockage-securise.md @@ -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`. diff --git a/docs/specs/jalon-5-code-parental/03-acces-espace-parent.md b/docs/specs/jalon-5-code-parental/03-acces-espace-parent.md new file mode 100644 index 0000000..9201e5e --- /dev/null +++ b/docs/specs/jalon-5-code-parental/03-acces-espace-parent.md @@ -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. diff --git a/docs/specs/jalon-5-code-parental/README.md b/docs/specs/jalon-5-code-parental/README.md new file mode 100644 index 0000000..0df0ced --- /dev/null +++ b/docs/specs/jalon-5-code-parental/README.md @@ -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. diff --git a/docs/specs/jalon-6-limites/01-domaine-limites.md b/docs/specs/jalon-6-limites/01-domaine-limites.md new file mode 100644 index 0000000..e444b03 --- /dev/null +++ b/docs/specs/jalon-6-limites/01-domaine-limites.md @@ -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. diff --git a/docs/specs/jalon-6-limites/02-avertissements.md b/docs/specs/jalon-6-limites/02-avertissements.md new file mode 100644 index 0000000..bc525f4 --- /dev/null +++ b/docs/specs/jalon-6-limites/02-avertissements.md @@ -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). diff --git a/docs/specs/jalon-6-limites/03-fin-session.md b/docs/specs/jalon-6-limites/03-fin-session.md new file mode 100644 index 0000000..fdd474a --- /dev/null +++ b/docs/specs/jalon-6-limites/03-fin-session.md @@ -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. diff --git a/docs/specs/jalon-6-limites/README.md b/docs/specs/jalon-6-limites/README.md new file mode 100644 index 0000000..666d31e --- /dev/null +++ b/docs/specs/jalon-6-limites/README.md @@ -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.