Folders over the 50-item cap now show a "Mas..." item that reveals the next page on tap, instead of silently dropping the rest. Paging slices the cheap raw list before building any MediaItem, so items beyond the requested page are never resolved (art, title) -- proven by a call-count test. Also swaps the raw SAF content:// URI shown in settings for a parsed, human-readable folder name with a localized fallback across all 13 locales. servicio_audio.dart is untouched; this stays entirely within the local-music tree/dispatch layer.
75 lines
8.6 KiB
Markdown
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.
|