Files
pluriwave/openspec/changes/archive/2026-07-19-android-auto-local-music-paging/tasks.md
T
FreeTLab e030a0975d docs(openspec): archive android-auto-local-music-paging
Merges its delta requirements into the android-auto-media base spec.
This closes out Phase-1 polish for local music; Phase 2 (metadata,
sort/filter, real art) and Phase 3 (subfolder scoping, shuffle)
remain the only planned future work for this feature.
2026-07-19 22:21:45 +02:00

75 lines
8.6 KiB
Markdown

# Tasks: Android Auto Local Music — On-Demand Paging + Friendly Folder Name
## Review Workload Forecast
| Field | Value |
|-------|-------|
| Estimated changed lines | ~480-600 (prod ~170: navegacion_auto.dart ~120, musica_local_auto.dart ~45, pantalla_ajustes.dart ~8; l10n ~26 across 13 arb; tests ~300-350: paging/id/Más/call-count/hijosMusicaLocal + friendly-name + 8 call-site updates) |
| 400-line budget risk | Medium |
| Chained PRs recommended | Yes |
| Suggested split | PR 1 → PR 2 → PR 3 (each independently <400 lines) |
| Delivery strategy | single PR with `size:exception` (user explicitly overrode the chained-PR recommendation) |
| Chain strategy | n/a — single PR |
Decision needed before apply: Resolved — single PR, `size:exception`.
Chained PRs recommended: Yes (not taken; user chose single-PR exception)
400-line budget risk: Medium (accepted)
### Suggested Work Units
| Unit | Goal | Likely PR | Notes |
|------|------|-----------|-------|
| 1 | `paginaDe<T>`/`hayPaginaSiguiente` + `carpeta_local_pag:` id encode/decode, fully unit-tested | PR 1 | Foundation; no behavior change yet; ~200 lines |
| 2 | `itemsLocales` paged rewrite + "Más…" item + `hijosMusicaLocal` paged branch, call-count/boundary tests | PR 2 | Depends on PR 1; user-visible paging behavior; ~250 lines |
| 3 | `nombreCarpetaDesdeUri` + 13-locale `localMusicFolderGenericName` + settings UI wiring | PR 3 | Independent of PR 1/2; ~150 lines |
Implemented as a single PR per delivery strategy — all 3 units applied in one pass, work-unit commit boundaries preserved conceptually in the diff structure (paging primitives → paged id scheme → itemsLocales/hijosMusicaLocal → friendly name → l10n → settings UI).
## Phase 1: Paging Primitives (`lib/servicios/navegacion_auto.dart`)
- [x] 1.1 RED: test `paginaDe<T>` — mid-list slice, empty list, page beyond range, partial last page (spec: no permanent unreachability)
- [x] 1.2 GREEN: implement top-level `paginaDe<T>` (skip/take, generic per ADR-6)
- [x] 1.3 RED: test `hayPaginaSiguiente` exact boundary (`total == (pagina+1)*tamano` → false; `>` → true)
- [x] 1.4 GREEN: implement top-level `hayPaginaSiguiente`
## Phase 2: Paged Media-ID Scheme
- [x] 2.1 RED: test `esCarpetaLocalPaginadaMediaId`/`paginaCarpetaLocalDesde` — collision-free vs `emisora:`/`grupo:`/`eq_preset:`/`carpeta_local:`/`pista:` + bare root ids; docId containing `:`/`/`; empty-tail root page (`carpeta_local_pag:1:`)
- [x] 2.2 GREEN: implement `_prefijoCarpetaLocalPaginada`, `esCarpetaLocalPaginadaMediaId`, `paginaCarpetaLocalDesde` (strip-by-length, split-on-first-`:`, per ADR-1)
## Phase 3: Paged `itemsLocales` + "Más…" Item
- [x] 3.1 RED: test call-count invariant via `@visibleForTesting construirItem` spy — 200 `NodoLocal`s, page 0/1/3 each call spy exactly `min(50, remaining)` times (ADR-3 regression guard)
- [x] 3.2 RED: test "Más…" boundary — exactly 50 (no "Más…"), 51-100 (one "Más…", then last page none), 100+ chains 3+ pages, all items reached
- [x] 3.3 RED: test "Más…" item shape (id `carpeta_local_pag:<n+1>:<docId>`, `title='Más…'`, `playable:false`, no `artUri`, round-trips via `paginaCarpetaLocalDesde`); root paging (`documentIdPadre:''`); stale page beyond range → `[]`, no "Más…", no throw
- [x] 3.4 GREEN: extend `itemsLocales(nodos, {required documentIdPadre, pagina=0, tamano=_maxItemsCarpetaLocal, construirItem})` — sort full list, `paginaDe` the cheap list, map only the slice, append `_itemMasLocal` via `hayPaginaSiguiente`; add `_tituloMasLocal='Más…'`, `_itemMasLocal` (hardcoded Spanish per ADR-5 — no arb key)
- [x] 3.5 Update existing `itemsLocales(nodos)` call sites in `test/servicios/navegacion_auto_test.dart` (~lines 384-484) to pass `documentIdPadre` — mechanical signature adaptation. One pre-existing 60-item test asserted the OLD cap-truncate behavior (exactly 50 items, no more) — this is superseded by the spec's paging requirement (Modified Requirement "Local Music Folder Item Cap and Paging"), so that specific assertion was deliberately rewritten (not just signature-patched) into two new boundary tests (`exactamente 50 items`, `51 a 100 items`) that assert the NEW paging behavior; every other call site kept identical assertions with only `documentIdPadre` added.
## Phase 4: Wire Paged ID into `hijosMusicaLocal`
- [x] 4.1 RED: test `hijosMusicaLocal` routes `carpeta_local_pag:<n>:<docId>` to `(docId, n)` and returns that page; non-local id still `null`; `null` fuente → `[]`; thrown error → `[]`
- [x] 4.2 GREEN: add paged branch inside `hijosMusicaLocal` (ADR-7 — `servicio_audio.dart` unchanged; `getChildren`/`playFromMediaId` need no edit)
## Phase 5: Friendly Folder Name (`lib/servicios/musica_local_auto.dart`)
- [x] 5.1 RED: test `nombreCarpetaDesdeUri` — internal storage (`primary:Music/MyFolder``MyFolder`), SD card (`1A2B-3C4D:Music``Music`), nested `/`-segment, storage-root empty tail (`primary:`)→generic fallback, unparseable→generic fallback, never raw URI/empty (ADR-4)
- [x] 5.2 GREEN: implement pure `nombreCarpetaDesdeUri(String, {required String nombreGenerico}) -> String`. Signature refinement vs. the terse task description (mirrors the same abbreviation gap task 3.4 had for `itemsLocales`): the generic-fallback text must be the LOCALIZED `localMusicFolderGenericName` string per ADR-4/ADR-5, but this function has no `BuildContext`/`AppLocalizations` access by design (pure, unit-testable without a widget tree). So the fallback text is threaded in as a plain `String nombreGenerico` parameter — the call site (`pantalla_ajustes.dart`, which already holds `l10n`) supplies `l10n.localMusicFolderGenericName`. Precedent: `pantalla_reproductor.dart`'s `_formatearDuracion(AppLocalizations l10n, ...)` pattern, minus the Flutter-generated-class coupling.
## Phase 6: Localization
- [x] 6.1 Added `localMusicFolderGenericName` key (phone UI only, per ADR-5) to all 13 `lib/l10n/app_{ar,bn,de,en,es,fr,hi,id,it,ja,pt,ru,zh}.arb` — no "Más…" arb key anywhere. Values matched each locale's existing `localMusicFolder*`/`localMusicChoosePath` register precedent (e.g. es "Carpeta seleccionada", en "Selected folder", de "Ausgewählter Ordner", fr "Dossier sélectionné", ar "مجلد محدد", ja "選択したフォルダー", zh "已选文件夹", etc).
## Phase 7: Settings UI Wiring
- [x] 7.1 `_SeccionMusicaLocal` (`lib/pantallas/pantalla_ajustes.dart` ~line 361): renders `nombreCarpetaDesdeUri(carpeta, nombreGenerico: l10n.localMusicFolderGenericName)` instead of raw `carpeta` when configured; unconfigured case (`null`/empty) keeps `l10n.localMusicFolderNotConfigured`. Widget test added in `test/pantallas/pantalla_ajustes_test.dart` (group "`_SeccionMusicaLocal` — friendly folder name (Phase 7)") but COULD NOT be executed in this sandbox — see Known Deviations below; this is a pre-existing environment gap, not introduced by this change.
## Phase 8: Regression / Manual Follow-Up
- [x] 8.1 Confirmed `esArchivoAudio` tests (`musica_local_auto_test.dart`, 10/10 pass) and `raiz()`/`esCarpetaLocalMediaId`/`idCarpetaLocalDesde`/plain `carpeta_local:` `hijosMusicaLocal` tests (`navegacion_auto_test.dart`, 85/85 pass) pass with no behavior change — this change only extends `itemsLocales` internals and adds a new branch, root-folder-count/dispatch logic untouched.
- [x] 8.2 DEVIATED (manual follow-up, not executable here): `flutter analyze`, `flutter test` (full suite), `flutter gen-l10n` — run manually, same convention as prior changes.
## Known Deviations (apply-time)
1. **`pantalla_ajustes_test.dart` Phase 7 widget tests could not be run in this sandbox.** The tracked `lib/l10n/gen/app_localizations.dart` is stale — it predates even the PARENT local-music change (missing `localMusicFolderNotConfigured`, `localMusicSectionTitle`, etc., which the file already referenced before this apply pass touched anything). Regenerating it requires `flutter gen-l10n`, which is documented to hang in this environment and was explicitly out of scope for this apply run. This is a pre-existing gap, confirmed by compiler errors pointing exclusively at PRE-EXISTING lines (320-383) before any of my new code was even reached. The new tests are written and structurally correct (seed `SharedPreferences` with a `musica_local_uri`, assert the friendly name renders and the raw `content://` URI never does) and will pass once `flutter gen-l10n` is run as part of the normal manual follow-up (task 8.2).
2. **`itemsLocales`' 60-item cap test was intentionally rewritten, not just signature-patched** (see 3.5) — this is a deliberate behavior change mandated by the spec delta (Modified Requirement "Local Music Folder Item Cap and Paging": cap-and-truncate → cap-and-page), not a regression.