329 lines
18 KiB
Markdown
329 lines
18 KiB
Markdown
# V1 — Médiathèque (refonte de l'onglet Ressources)
|
||
|
||
> **Statut** : **implémenté le 2026-09-02.** Backend + front livrés, `dotnet test` et
|
||
> `flutter build web` verts. Reste à valider à l'œil dans le navigateur (checklist §6).
|
||
> **Autonome.** Aucun appel à un modèle, aucune clé API nouvelle, aucun coût variable.
|
||
> Ne dépend ni du Studio ni de la migration Postgres. **Peut se livrer seul.**
|
||
>
|
||
> Le Studio (V2) se greffe dessus plus tard sans le réécrire — voir
|
||
> [v2/studio-plan.md §3.0](v2/studio-plan.md). Ce document est **exécutable tel quel** :
|
||
> il ne suppose aucune connaissance de la conversation de conception.
|
||
|
||
---
|
||
|
||
## 0. Pourquoi ce lot existe
|
||
|
||
Il est né en cherchant où loger les images générées par le futur Studio. La réponse a été
|
||
« nulle part de nouveau, l'écran existant suffit » — **à condition de le refondre**, parce qu'à
|
||
312 ressources il est illisible.
|
||
|
||
Les défauts sont ceux d'aujourd'hui, mesurés dans le code. Le lot les corrige, plus deux bugs.
|
||
|
||
| Défaut | Où |
|
||
|---|---|
|
||
| Tuiles carrées de 160 px, affichées d'un bloc sans ordre ni groupement | `resource_body_grid.dart:78` — `SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 160, childAspectRatio: 1.0)` |
|
||
| Chips de type en `Wrap` — **un seul axe de filtre** | `resource_body_grid.dart:133` |
|
||
| Ni tri, ni groupement, ni sélection multiple, ni action en lot | absent |
|
||
| Popup 520 px, `borderRadius: 20`, aperçu encadré d'une bordure grise de **3 px à `borderRadius: 30`** — hors de l'échelle de l'app (5/8/10) | `show_resource_popup.dart:20, 50-55` |
|
||
| Champ Label **au-dessus** de l'aperçu : on nomme avant d'avoir vu | `show_resource_popup.dart:36` |
|
||
| Quatre `RoundedButton` de même poids dans un `Wrap` — destructif collé au primaire, ça se stacke | `show_resource_popup.dart:67-110` |
|
||
| Aucune métadonnée affichée, alors que `SizeBytes`, `FileName`, `DateCreation`, `Type` sont en base | — |
|
||
| Impossible de savoir **où** une ressource est utilisée, ni si elle l'est | — |
|
||
| 🐛 **Le téléchargement force `.json`** quel que soit le type : un PNG se télécharge en `label.json` | `show_resource_popup.dart:91` |
|
||
| 🐛 **`Resource.FileName` n'est pas dans `ToDTO()`** — le front n'a pas de quoi nommer le fichier | `Data/Resource.cs` |
|
||
|
||
---
|
||
|
||
## 1. Périmètre
|
||
|
||
### Dans le lot
|
||
|
||
1. Renommage **Ressources → Médiathèque** (libellé de menu et titre d'écran).
|
||
2. **Rail de facettes cumulables** avec compteurs : Type · Usage · Origine · Configuration.
|
||
3. **Tri** (date, nom, poids, usages) et **groupement par mois**, bascule grille ↔ liste.
|
||
4. **Sélection multiple** et actions en lot (télécharger, supprimer).
|
||
5. **Compteur d'usages sur la vignette.**
|
||
6. Le détail passe de la modale au **panneau latéral** : aperçu d'abord, métadonnées réelles,
|
||
liste « Utilisée dans » cliquable, boutons hiérarchisés, **Supprimer désactivé tant que la
|
||
ressource sert**.
|
||
7. Backend : **index inverse des usages** (2 endpoints) + enrichissement de `ResourceDTO`.
|
||
8. Les deux bugs ci-dessus.
|
||
|
||
### Hors du lot — explicitement
|
||
|
||
- Toute génération IA (onglet « Générer », crédits, provenance) → **V2**.
|
||
- Le filtre « Origine › Générées par IA » : la facette existe dans le rail, mais **inutile en V1**
|
||
puisque rien n'est généré. **La coder quand même** (elle lit `AiProvenance`, absent en V1 → 0
|
||
résultat) ou la masquer tant que `studioEnabled == false`. **Recommandé : la masquer**, pour ne
|
||
pas afficher une facette vide.
|
||
- Upload serveur (`IResourceBlobService.UploadAsync`) → V2, lot 0. L'upload reste navigateur → Firebase.
|
||
- Thumbnails / variantes / CDN → hors périmètre, aucune des deux versions n'en a besoin.
|
||
- `GuidedStep.ImageUrl` → `ImageResourceId` : **V2**. Conséquence assumée en V1, voir §3.4.
|
||
|
||
---
|
||
|
||
## 2. Backend — `manager-service`
|
||
|
||
### 2.1 Enrichir `ResourceDTO`
|
||
|
||
`Data/Resource.cs`, méthode `ToDTO()`. Champs à ajouter :
|
||
|
||
```csharp
|
||
fileName = FileName, // existe en base, jamais exposé — corrige le bug du .json
|
||
width = Width, // nouvelle colonne, nullable
|
||
height = Height, // nouvelle colonne, nullable
|
||
usageCount = null, // rempli seulement par les endpoints qui le calculent (§2.2)
|
||
```
|
||
|
||
**Migration** : `Resource.Width` et `Resource.Height`, `int?`. Nullables et **non backfillées** —
|
||
l'UI n'affiche les dimensions que si elles sont présentes. Elles sont renseignées à la création par
|
||
manager-app, qui les connaît déjà (`ImageCompressor` décode l'image avant de la compresser).
|
||
|
||
⚠️ `FileName` peut être nul sur les ressources anciennes. Le front retombe alors sur
|
||
`label` + l'extension déduite du `Type`.
|
||
|
||
### 2.2 Index inverse des usages
|
||
|
||
**Le mécanisme existe déjà** : `GetReferencedResourceIds(string language = null)` est implémentée
|
||
sur les **13 sous-types de `Section`**, plus `GuidedPath` et `GuidedStep`. C'est elle qui fait
|
||
marcher l'export offline (`ConfigurationController.Export`). L'index inverse consiste à la
|
||
parcourir dans l'autre sens.
|
||
|
||
```
|
||
GET /api/Resource/{id}/usages
|
||
→ [ { kind, id, label, configurationId, configurationLabel, sectionId, field, path } ]
|
||
|
||
GET /api/Resource/usage-map?instanceId={id}
|
||
→ { usages: { "<resourceId>": { count, configurationIds: [] }, ... },
|
||
configurations: { "<configurationId>": "<label>", ... } }
|
||
```
|
||
|
||
`sectionId` (ajouté à l'implémentation) porte la section à ouvrir quand on clique une ligne
|
||
« Utilisée dans » — nul pour `kind = Configuration`.
|
||
|
||
La forme de `usage-map` s'est enrichie par rapport à la spécification (un simple compteur) :
|
||
la facette **Configuration** du rail a besoin de savoir **dans quelles configurations** une
|
||
ressource sert, et de leurs libellés. Le tout en **une** requête, comme prévu.
|
||
|
||
`kind` ∈ `Section` | `GuidedPath` | `GuidedStep` | `Configuration`.
|
||
`path` est la chaîne lisible affichée au front : `"Escape game › Parcours › image"`.
|
||
|
||
Implémentation recommandée : un `ResourceUsageService` qui construit la carte complète pour une
|
||
instance en une passe, et la met en cache mémoire courte (30 s) — `usage-map` est appelée à chaque
|
||
ouverture de l'écran, et l'écran est ouvert souvent.
|
||
|
||
#### ⚠️ Quatre pièges, à traiter explicitement
|
||
|
||
1. ~~**Union sur toutes les langues.**~~ **Vérifié dans le code à l'implémentation : faux.**
|
||
`SectionText.ResourceIds` et `ResourceIdsFromValues` filtrent par `language == null || …`, et
|
||
`GetReferencedResourceIds(string language = null)` est documentée « null rend toutes les langues ».
|
||
**Appeler la méthode sans argument suffit** — pas de boucle sur `Configuration.Languages`. Le
|
||
danger décrit (une image posée qu'en NL comptée orpheline) est réel si on passe une langue :
|
||
c'est ce que fige le test `ImageUsedOnlyInDutch_IsNotOrphan`.
|
||
2. **`Configuration.ImageId` et `LoaderImageId` sont hors sections.** Voir
|
||
`ConfigurationController.cs:407-414`, qui les ajoute séparément dans l'export. Les inclure, sans
|
||
quoi l'image d'accueil d'une configuration s'afficherait « jamais utilisée ».
|
||
3. **`GuidedStep.ImageUrl` est une URL absolue, pas un id** — commentaire explicite en
|
||
`GuidedStep.cs:74`. Une image posée là **sera comptée orpheline** en V1. C'est une limite connue :
|
||
la migration en `ImageResourceId` est en V2. **À écrire dans le tooltip du filtre**, sinon un
|
||
utilisateur supprimera une image utilisée par une étape de parcours.
|
||
|
||
4. **Un `GuidedPath` peut pendre d'un `SectionEvent`** (`GuidedPath.SectionEventId`), et
|
||
`SectionEvent.GetReferencedResourceIds` **ne descend pas dans les parcours** — seul
|
||
`SectionParcours` le fait. Piège trouvé à l'implémentation, absent de la spécification initiale.
|
||
Les `GuidedPath` sont donc parcourus **séparément**, et volontairement **pas chargés** sur
|
||
`SectionParcours` (sans quoi chaque usage compterait double). Figé par `GuidedPathOnEvent_IsWalked`.
|
||
|
||
> ℹ️ **`AsNoTracking` est proscrit** dans ce service : les colonnes `jsonb` passent par un
|
||
> convertisseur sans `ValueComparer`, et le provider EF InMemory des tests rend alors des
|
||
> collections **vides** sur une requête détachée — les tests d'usage seraient verts sans rien couvrir.
|
||
|
||
### 2.3 Suppression protégée
|
||
|
||
`DELETE /api/Resource/{id}` : renvoyer **409** avec la liste des usages si `usageCount > 0`.
|
||
Aujourd'hui rien ne protège. Le front désactive déjà le bouton, mais la règle doit vivre côté serveur
|
||
— l'appel peut venir d'ailleurs.
|
||
|
||
Ajouter `DELETE /api/Resource/bulk` (corps : `{ ids: [] }`) pour l'action en lot, avec la même règle
|
||
par élément et un rapport partiel : `{ deleted: [], refused: [{id, usageCount}] }`.
|
||
|
||
---
|
||
|
||
## 3. Front — `manager-app`
|
||
|
||
### 3.1 Renommage
|
||
|
||
| Où | Aujourd'hui | Après |
|
||
|---|---|---|
|
||
| `l10n/app_fr.arb:57` | `"menuResources": "Ressources"` | `"Médiathèque"` |
|
||
| `app_en.arb`, `app_nl.arb` | idem | `"Media library"` / `"Mediatheek"` |
|
||
| `main_screen.dart:302` | `case 'resources': return l.menuResources;` | inchangé (la clé suffit) |
|
||
|
||
⚠️ La route reste `/main/resources` — **ne pas la renommer**, les liens et les habitudes existent.
|
||
Le libellé change, pas l'identifiant.
|
||
|
||
### 3.2 Fichiers touchés
|
||
|
||
| Fichier | Action |
|
||
|---|---|
|
||
| `Screens/Resources/resources_screen.dart` (469 l. au total du dossier) | Garde son rôle de conteneur et son `FutureBuilder`. Charge en plus la `usage-map`. **Ne pas casser sa double vie** : il est aussi embarqué par `showSelectResourceModal` (§3.3) |
|
||
| `Screens/Resources/resource_body_grid.dart` | Le gros du travail : rail de facettes, tri, groupement, sélection, nouvelles vignettes |
|
||
| `Screens/Resources/show_resource_popup.dart` | **Supprimé.** À l'implémentation, `grep` a montré que `showResource()` n'était appelée que par `resources_screen.dart` : garder un export mort pour « ne pas casser les appelants » n'avait pas d'objet. Remplacé par `resource_detail_panel.dart` |
|
||
| `Screens/Resources/resource_detail_panel.dart` | **Nouveau** — le panneau latéral (aperçu, nom, métadonnées, « Utilisée dans », pied hiérarchisé) |
|
||
| `Screens/Resources/resource_formatting.dart` | **Nouveau** — extension de téléchargement, poids, mois, dates |
|
||
| `Screens/Resources/resource_download.dart` | **Nouveau** — `downloadResource()`, la correction du `.json` en dur |
|
||
| `Helpers/ImageCompressor.dart` | `CompressedImage` expose `width`/`height` : c'est le seul endroit qui décode l'image avant l'upload |
|
||
| `Screens/Resources/select_resource_modal.dart` | Inchangé si possible — voir §3.3 |
|
||
| `constants.dart` | Aucun token nouveau à créer : `kSurface*`, `kInk*`, `kLine*`, `kSpace*`, `kRadius*` couvrent tout |
|
||
|
||
### 3.3 ⚠️ Le piège central : un écran, deux vies
|
||
|
||
```dart
|
||
// select_resource_modal.dart:24
|
||
child: ResourcesScreen(
|
||
isAddButton: isAddButton,
|
||
isSelect: isSelect,
|
||
isRemoveButton: isRemoveButton,
|
||
onGetResult: (ResourceDTO? resource) { ... },
|
||
resourceTypes: resourceTypes,
|
||
),
|
||
```
|
||
|
||
**`showSelectResourceModal` embarque `ResourcesScreen`.** L'onglet Médiathèque et le sélecteur de
|
||
champ de n'importe quel éditeur sont **le même widget**. Toute régression touche donc les 13 types de
|
||
section, les POI et les étapes d'un coup.
|
||
|
||
Règles à respecter :
|
||
|
||
- En mode `isSelect: true`, un clic sur une vignette **sélectionne** (comportement actuel), il
|
||
n'ouvre pas le panneau de détail.
|
||
- Le rail de facettes doit être **réductible ou masqué** dans la modale : elle fait déjà
|
||
`size.width * 0.85`, et un rail de 194 px y est acceptable — mais le vérifier à 1280 px de large.
|
||
- Le mode sélection multiple est **désactivé** quand `isSelect: true`.
|
||
- `resourceTypes` filtre déjà en amont : la facette Type doit se limiter aux types reçus, comme
|
||
aujourd'hui (`resource_body_grid.dart:42`).
|
||
|
||
**Test de non-régression obligatoire** : ouvrir un champ image d'une section, choisir une ressource,
|
||
enregistrer. C'est le chemin le plus emprunté de toute l'app.
|
||
|
||
### 3.4 Le rail de facettes
|
||
|
||
```
|
||
TYPE Tout · Images · Audio · Vidéo · PDF (compteurs)
|
||
USAGE Utilisées · Jamais utilisées
|
||
ORIGINE (masqué en V1 — voir §1)
|
||
CONFIGURATION une ligne par configuration + « Aucune »
|
||
```
|
||
|
||
- **Cumulables entre groupes** (ET), une seule valeur par groupe. Recliquer la valeur active
|
||
l'efface ; « Tout » efface l'axe Type.
|
||
- Les compteurs portent sur le corpus complet de l'instance, pas sur le résultat filtré.
|
||
- Une barre au-dessus de la grille récapitule les filtres actifs + « tout effacer ».
|
||
- **Tooltip sur « Jamais utilisées »** : mentionner la limite `GuidedStep.ImageUrl` (§2.2 piège 3).
|
||
|
||
### 3.5 La grille
|
||
|
||
- `SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 150)` mais **ratio 4/3 avec un pied
|
||
de carte** : nom, poids, origine, et le compteur d'usages en pastille sur la vignette
|
||
(`2 usages` / `libre`).
|
||
- Groupement par mois par défaut (en-tête `Septembre 2026`), replié quand le groupe est vide après filtrage.
|
||
- Tri : plus récentes (défaut), nom A→Z, poids décroissant, usages croissants.
|
||
- État vide explicite : « Aucune ressource ne correspond à ces filtres ».
|
||
|
||
### 3.6 Le panneau de détail
|
||
|
||
Remplace la modale. Ordre imposé, de haut en bas :
|
||
|
||
1. **Aperçu** — d'abord. Pas de bordure grise de 3 px ; `kRadiusCard` et `kLineSoft`.
|
||
2. **Nom** — champ éditable, *après* l'aperçu.
|
||
3. **Métadonnées** : type, dimensions (si présentes), poids, date, auteur.
|
||
4. **Utilisée dans** — liste cliquable, chaque ligne navigue vers le contenu. Si vide : un encart
|
||
ambre « Jamais utilisée. Aucun visiteur ne la verra ».
|
||
5. **Pied d'actions**, hiérarchisé : `Enregistrer` (primaire, à gauche) · `Remplacer le fichier` ·
|
||
`Télécharger` · `Supprimer` (destructif, isolé à droite, **désactivé si `usageCount > 0`**).
|
||
|
||
Le panneau reste ouvert quand on clique une autre vignette — c'est le geste réel quand on trie une
|
||
médiathèque.
|
||
|
||
### 3.7 Correction du téléchargement
|
||
|
||
```dart
|
||
// show_resource_popup.dart:91 — aujourd'hui
|
||
anchorElement.download = '${resourceDTO.label}.json';
|
||
```
|
||
|
||
Devient : `resource.fileName` s'il existe, sinon `label` + l'extension déduite de `Type`
|
||
(`Image → .jpg`, `Audio → .mp3`, `Video → .mp4`, `PDF → .pdf`, `JSON → .json`, `Word → .docx`,
|
||
`PowerPoint → .pptx`, `Text → .txt`).
|
||
|
||
---
|
||
|
||
## 4. Ce que la V1 laisse volontairement en place
|
||
|
||
- L'upload passe toujours par le navigateur → Firebase (`resources_screen.dart:247-256`).
|
||
- Pas de `AiProvenance`, pas de badge « généré par IA », pas de crédits.
|
||
- `ResourceType` inchangé — **ne rien ajouter à l'enum**, il est persisté en int.
|
||
- La jauge de stockage du menu garde son bug de rafraîchissement (relevé le 2026-08-25,
|
||
`quota_bars_widget.dart:23`). **Hors périmètre, mais tentant** : le corriger ici coûte peu et
|
||
l'écran touche justement au stockage. À arbitrer au démarrage du lot.
|
||
|
||
---
|
||
|
||
## 5. Ordre d'exécution suggéré
|
||
|
||
> **Suivi à l'implémentation (2026-09-02)** — les 8 étapes sont codées. Ce qui reste est
|
||
> de la vérification à l'œil : la checklist §6 n'a pas été passée dans un navigateur.
|
||
> Non fait, délibérément : la jauge de stockage du menu (§4), laissée hors périmètre.
|
||
|
||
| # | Étape | Vérifiable par |
|
||
|---|---|---|
|
||
| 1 | Migration `Width`/`Height` + `fileName` dans `ToDTO()` + correction du `.json` | `dotnet test`, puis télécharger un PNG et vérifier son extension |
|
||
| 2 | `ResourceUsageService` + les 2 endpoints, avec les 3 pièges traités | Un test qui pose une image en NL seulement et vérifie qu'elle n'est **pas** orpheline |
|
||
| 3 | `409` sur `DELETE` si utilisée, + `DELETE /bulk` | Test d'intégration |
|
||
| 4 | Renommage + rail de facettes + compteurs | `flutter build web`, puis l'écran |
|
||
| 5 | Tri, groupement, état vide | l'écran |
|
||
| 6 | Panneau de détail + « Utilisée dans » + suppression protégée | l'écran |
|
||
| 7 | Sélection multiple + actions en lot | l'écran |
|
||
| 8 | **Non-régression du sélecteur de ressource** | Ouvrir un champ image d'une section, choisir, enregistrer |
|
||
|
||
⚠️ `flutter analyze` ne suffit pas — seul `flutter build web` dit la vérité (leçon consignée dans
|
||
[STATUS.md §1bis](STATUS.md)).
|
||
|
||
---
|
||
|
||
## 6. Checklist de test
|
||
|
||
> À passer dans un navigateur. Aucune de ces cases n'est cochée par le code seul —
|
||
> `flutter build web` dit que ça compile, pas que ça marche.
|
||
|
||
- [ ] Le menu affiche « Médiathèque », la route reste `/main/resources`
|
||
- [ ] Deux facettes de groupes différents se cumulent (ex. Audio + Jamais utilisées)
|
||
- [ ] Recliquer une facette active l'efface ; « Tout » efface l'axe Type
|
||
- [ ] Les compteurs du rail ne bougent pas quand on filtre
|
||
- [ ] Un groupe de mois vide après filtrage disparaît
|
||
- [ ] Le compteur d'usages sur la vignette correspond au « Utilisée dans » du panneau
|
||
- [ ] Une image utilisée **uniquement en NL** n'apparaît pas dans « Jamais utilisées »
|
||
- [ ] L'image d'accueil d'une configuration n'apparaît pas dans « Jamais utilisées »
|
||
- [ ] Une image posée sur `GuidedStep.ImageUrl` apparaît orpheline **et le tooltip l'explique**
|
||
- [ ] `Supprimer` est désactivé sur une ressource utilisée, et le serveur renvoie 409 si on force
|
||
- [ ] Suppression en lot : les utilisées sont refusées, les autres supprimées, le rapport est lisible
|
||
- [ ] Un PNG se télécharge en `.png`, un MP3 en `.mp3`
|
||
- [ ] Une ressource ancienne sans `FileName` se télécharge quand même avec la bonne extension
|
||
- [ ] **Le sélecteur de ressource d'un champ de section fonctionne toujours** — sélection, annulation, ajout
|
||
- [ ] `flutter build web` passe
|
||
|
||
---
|
||
|
||
## 7. Ce que la V2 viendra greffer
|
||
|
||
Pour que l'implémentation V1 ne ferme aucune porte :
|
||
|
||
| V2 ajoutera | Point d'ancrage à prévoir en V1 |
|
||
|---|---|
|
||
| Onglet « Générer » dans le sélecteur | `ResourceTab` / `showNewResource` gardent une structure à onglets |
|
||
| Facette « Générées par IA » | Le rail est déjà générique : une facette de plus, pas une refonte |
|
||
| Badge et bloc provenance | Le pied de vignette et le panneau ont la place ; lire `AiProvenance` nullable |
|
||
| Console audio (narrateurs) | Vit dans `Configuration › Personnages`, **pas ici** |
|
||
|
||
Rien de tout ça ne demande de revenir sur la V1.
|