875 lines
74 KiB
Markdown
875 lines
74 KiB
Markdown
# Tasks: Functional Redesign (`rediseno-funcional`)
|
||
|
||
> Reads (authoritative): `proposal.md`, `design.md`, `specs/*/spec.md` in this folder.
|
||
> Binding corrections from the design phase are applied throughout (see table below) — they override the proposal
|
||
> where they conflict with it.
|
||
> **Delivery model**: 18 work-unit **commits** on one branch, `feat/rediseno-funcional` — NOT chained PRs (Engram
|
||
> `sdd/rediseno-funcional/delivery-strategy`, id 2504, a user decision that supersedes the proposal's
|
||
> feature-branch-chain recommendation). Conventional commits only. Never `Co-Authored-By` or AI attribution. Never
|
||
> push to `main`. The user opens one PR in the Gitea web UI at the end; this agent never opens PRs.
|
||
> **This artifact intentionally exceeds the default ~530-word task-budget**, for the same reason `proposal.md` and
|
||
> `design.md` do: 18 work units each need a commit message, a RED/GREEN/REFACTOR breakdown, named test files, spec
|
||
> traceability, and a verification command. Collapsing that to fit a budget would hide exactly the detail a reviewer
|
||
> needs. Tables and checklists are used throughout to keep it scannable despite the length.
|
||
> **WU12 does not exist** — the native EQ band-count spike was resolved before planning closed (Engram id 2498, "keep
|
||
> 5 bands"). Numbering skips 11→13 on purpose; this is not a gap.
|
||
> **WU15b was added mid-apply, not planned upfront** — WU15 shipped `PantallaGrabaciones` (the recordings library)
|
||
> fully tested but reachable from nowhere in the app. WU15b (below, after WU15's section) is the coordinator-ruled fix
|
||
> that wires it into Settings navigation. It is small and does not change the 18-commit delivery model's shape.
|
||
> Strict TDD is ON. Runner: `flutter test`. `flutter analyze` and a **scoped** `dart format` gate every commit.
|
||
> **`flutter build` is never run.**
|
||
>
|
||
> ## ⚠️ NEVER run bare `dart format .`
|
||
>
|
||
> Verified on this machine: `dart format .` reformats **27 pre-existing files unrelated to any work unit**
|
||
> (`Formatted 141 files (27 changed)`). Cause is formatter version skew — this machine runs Dart SDK 3.12.0 while
|
||
> the repo's committed formatting came from another version. Running it bare would silently break four hard success
|
||
> criteria at once: the empty-`git diff` guards on `navegacion_auto.dart`, `servicio_ecualizador.dart` and
|
||
> `servicio_audio.dart`, plus "`pantalla_alarma_sonando_dismiss_guard_test.dart` passes unmodified".
|
||
>
|
||
> **Always scope the formatter to the files this work unit actually touched.** The verify commands below use:
|
||
>
|
||
> ```
|
||
> dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')
|
||
> ```
|
||
>
|
||
> Full detail: Engram `reference/dart-format-scope-hazard` (id 2511).
|
||
|
||
## Review Workload Forecast (per commit, not per PR)
|
||
|
||
| WU | Commit | Depends on | Est. changed lines | Budget risk | Exception |
|
||
|----|--------|-----------|---------------------|-------------|-----------|
|
||
| 1 | `feat(tokens): add design tokens, type scale, push scaffold, and root nav state` | — | 220-330 | Low | No |
|
||
| 2 | `test(auto): confirm Android Auto tree matches the redesign, no code changes` | — | 0 | None | No |
|
||
| 3a | `refactor(ajustes): split Settings AUDIO/EMISORAS into pushed detail screens` | 1 | ~~800-1000~~ → **REALIZED: 4,374** (2,760+ / 1,614-, 35 files) | High | **Yes — move-only diff** |
|
||
| 3b | `refactor(ajustes): split remaining Settings sections into pushed screens` | 3a | ~~2,500-3,200~~\* → **REALIZED: 2,190** (1,485+ / 705-, 28 files) | Medium-High | **Yes — move-only diff** |
|
||
| 4 | `feat(favoritos): replace stacked group panels with chip-filtered reorderable list` | 1 | 300-400 | Medium | Monitor |
|
||
| 5 | `feat(escuchar): replace discovery browser with embedded player and favorites grid` | 1, 4 | 350-450 | Medium | Monitor |
|
||
| 6 | `feat(buscar): add discovery landing state, filter pills, counter, and sort` | 5 | 350-500 | Medium-High | Monitor |
|
||
| 7 | `feat(paises): add country browser and extract shared radio transport` | 6 | 300-400 | Medium (test-heavy) | Monitor |
|
||
| 8 | `feat(alarmas): simplify alarm cards and add vacation summary row` | 1 | 400-550 | Medium-High | Monitor† |
|
||
| 9 | `feat(vacaciones): add vacation range manager screen` | 8 | 350-450 | Medium | Monitor |
|
||
| 10 | `feat(alarmas): rewrite alarm editor with inline time widget` | 8 | 500-650 | High | **Yes — indivisible new widget**‡ |
|
||
| 11 | `feat(alarma-sonando): restyle ringing screen, drop live countdown label` | 1 | 200-300 | Low (safety-critical review attention: High) | No |
|
||
| 13 | `feat(eq): restyle equalizer screen and add custom presets` | 3a | 400-550 | Medium-High | Monitor |
|
||
| 14 | `feat(reproductor): restructure full player with tool-tray and EQ sheet` | 13 | 450-600 | Medium-High | Monitor |
|
||
| 15 | `feat(grabaciones): add recordings library screen` | 3b | ~~300-400~~ → **REALIZED: 1,767** (1,767+ / 0-, 22 files) | Medium | Monitor§ |
|
||
| 15b | `fix(grabaciones): wire the recordings library into Settings navigation` | 15 | 60-100 | Low | No |
|
||
| 16 | `feat(connectivity): restyle offline and reconnect banners` | 1 | 150-250 | Low | No |
|
||
| 17 | `feat(bienvenida): add monetization-free welcome screen` | 1 | 150-200 | Low | No |
|
||
| 18 | `feat(i18n): add redesign strings and translate Escuchar rename to 11 locales` | all | ~0 eng. / 400-600 data | Medium (data volume, low logic risk) | No |
|
||
|
||
\* **Re-derived after WU3a landed.** The original 500-700 figure was scaled pro-rata from WU3a's *estimate*, which
|
||
turned out to be wrong by roughly 4x — so the old WU3b number inherited the same error and has been replaced.
|
||
WU3a's realized diff was **4,374 changed lines across 35 files** against an 800-1000 estimate. The gap was not
|
||
scope creep; the estimate simply never counted (a) the 7 matching new test files strict TDD requires, or (b) the
|
||
13 regenerated `lib/l10n/gen/app_localizations*.dart` files that any ARB edit forces. `lib/` alone, excluding
|
||
generated l10n, was 1,474+ / 1,214-, and the root shed 1,320 lines — so the "move-only" characterisation held; only
|
||
the arithmetic was wrong.
|
||
WU3b moves 5 sections rather than 7 and needs no new shared widget (`fila_ajuste.dart` already exists), hence
|
||
~2,500-3,200. **Estimating lesson for every remaining work unit: a strict-TDD commit carries its test files, and any
|
||
ARB touch drags 13 generated files with it. Estimates that count only `lib/` production code will read ~3-4x low.**
|
||
|
||
**Root line count, for reference**: `pantalla_ajustes.dart` went 1,897 → 788 in WU3a → **198** in WU3b. The "under
|
||
400 lines" success criterion was the WU3a+WU3b **combined** end state — WU3a owned only 7 of the 12 detail screens.
|
||
WU3b converted the remaining 5 (Grabaciones, Música local, Idioma, Backup, Info) and the root is now exactly 4
|
||
`GrupoAjustes` cards, well under the 400-line target.
|
||
† Record `size:exception` at apply time only if the realized diff exceeds ~500 lines; justification: "largest single
|
||
alarm-card + hero + vacation-summary restyle, not divisible without breaking the one-commit-per-work-unit rule."
|
||
‡ The proposal already isolates WU10 as "its own PR, never bundled" — splitting further would leave an unintegrated
|
||
commit (a widget with no consumer, or a sheet rewrite with no new editor).
|
||
§ **Re-derived after WU15 landed.** Same estimating lesson as WU3a/WU3b, at brand-new-screen scale: the 300-400
|
||
figure covered only the production screen, not (a) its matching ~470-line strict-TDD test file, (b) the 13
|
||
regenerated `lib/l10n/gen/*.dart` files (12 new ARB keys this time, since a genuinely new screen needs new copy,
|
||
unlike WU3a/WU3b's move-only reuse), or (c) the new `servicio_grabacion_radio.dart` additions and their own test
|
||
group. Realized 1,767 changed lines / 22 files, all additions (no deletions — nothing pre-existing was touched
|
||
beyond the 3 new `ServicioGrabacionRadio`/`EstadoGrabacion` methods). Not recorded as `size:exception` since the
|
||
commit is still a single, cleanly-scoped deliverable (one new screen, its one dependency, no split candidate).
|
||
|
||
```text
|
||
Decision needed before apply: No
|
||
Chained PRs recommended: No
|
||
Chain strategy: size-exception
|
||
400-line budget risk: High
|
||
```
|
||
|
||
**Why these values, not a fresh ask.** The delivery model was already decided by the user (Engram 2504), which
|
||
supersedes the proposal's own `feature-branch-chain` recommendation once the host was confirmed as Gitea (no `gh`,
|
||
agent cannot open PRs). That decision is functionally equivalent to an already-accepted `size:exception` at the
|
||
aggregate level — the one eventual PR will be reviewed commit-by-commit, which is why this forecast is expressed per
|
||
commit. Three commits (WU3a, WU3b, WU10) get an explicit `size:exception` because they cannot be reduced under 400
|
||
lines without violating the one-commit-per-work-unit rule; the rest are within or near budget individually.
|
||
|
||
**Parallelization note.** Delivery is sequential commits on one branch, so WUs land in the order below regardless of
|
||
true independence. For planning only: WU2 has no dependencies and may be pulled to any point (already noted in the
|
||
proposal). WU11, WU16, and WU17 each depend only on WU1 and are mutually independent of the WU3a-WU10 and WU13-15
|
||
chains — they could be reordered among themselves without breaking anything, but the sequence below is kept to match
|
||
the proposal's narrative and dependency table.
|
||
|
||
## Corrections From the Design Phase (applied throughout)
|
||
|
||
| # | Correction | Where applied |
|
||
|---|------------|---------------|
|
||
| 1 | `EstadoNavegacionRaiz` moves from WU5 into WU1 | WU1 tasks 1.6-1.7; WU5 task 5.3 only *consumes* `irA(...)` |
|
||
| 2 | WU3a/WU3b line estimates were structurally low (verbatim move = add+delete in `git diff`) | Forecast table above; `size:exception` recorded for both, justification "move-only diff" |
|
||
| 3 | Settings detail-screen count is **12**, not "~8" | WU3a (7 screens) + WU3b (5 screens) = 12, listed by name in tasks 3a.4-3a.5, 3b.2-3b.3 |
|
||
| 4a | Custom EQ preset persistence must live in a **new** file, never in `servicio_ecualizador.dart` | WU13 task 13.6, hazard box restated inline |
|
||
| 4b | Mini player must hide **visually** (`visible: false`), never leave the tree, to preserve the S3-R3 `didChangeDependencies` side effect | WU5 task 5.2, 5.6 |
|
||
| 5 | `MiniReproductor.altura` must be derived from laid-out height at apply time, not guessed | WU5 task 5.6 |
|
||
|
||
## Non-Negotiable Guards (master checklist — verify before the branch is considered done)
|
||
|
||
- [ ] `test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart` passes **unmodified** (touched by WU9, WU11 — if
|
||
a restyle appears to require editing it, STOP and escalate; do not edit the test).
|
||
- [ ] `git diff` is **empty** for `lib/servicios/navegacion_auto.dart` (WU2), `lib/servicios/servicio_ecualizador.dart`
|
||
(WU13), and the band-application block at `lib/servicios/servicio_audio.dart:749-762` (WU13).
|
||
- [ ] The equalizer renders exactly **5** sliders (WU13, task 13.1).
|
||
- [ ] No shipped string contains "PRO", a trial duration, or a price (WU17 task 17.2/17.6; WU18 task 18.6 final
|
||
whole-app grep).
|
||
- [ ] All 13 ARB files keep identical key sets (WU18 tasks 18.1, 18.5 — final parity check; WU1 only touches values
|
||
in 2 of the 13 files, so it cannot break parity on its own).
|
||
|
||
---
|
||
|
||
## WU1 — Tokens, push scaffold, Escuchar rename, root nav provider
|
||
|
||
**Commit**: `feat(tokens): add design tokens, type scale, push scaffold, and root nav state`
|
||
**Depends on**: — (first commit)
|
||
**Spec refs**: `app-navigation-shell` — Five-Tab Root Navigation, Push-Chrome on Second-Level Screens, Root-to-Root
|
||
Switching Without Push (provider only; consumption is WU5), Escuchar Tab Rename Preserves the ARB Key
|
||
**Verify**: `flutter test test/tema/ test/widgets/pluri_push_scaffold_test.dart test/estado/estado_navegacion_test.dart test/widget_test.dart && flutter analyze && dart format --set-exit-if-changed .`
|
||
**New tests**: `test/tema/pluriwave_typography_test.dart`, `test/widgets/pluri_push_scaffold_test.dart`, `test/estado/estado_navegacion_test.dart`
|
||
**Modified tests**: `test/tema/pluriwave_tokens_test.dart` (or equivalent), `test/widget_test.dart`
|
||
|
||
- [x] 1.1 RED — write `pluriwave_typography_test.dart` asserting the 6 named styles (`heroTime`, `sectionTitle`,
|
||
`screenTitle`, `cardTitle`, `bodyStrong`, `eyebrowLabel`) exist with their documented size/weight/letter-spacing
|
||
and that `PluriWaveTheme.dark()` registers the extension.
|
||
- [x] 1.2 RED — extend the tokens test asserting `PluriWaveTokens.listSurface`, `.liveGreen`, `.offlineAccent` exist
|
||
and participate in `lerp`.
|
||
- [x] 1.3 GREEN — add the 3 colour fields to `lib/tema/pluriwave_tokens.dart` (constructor, `copyWith`, `lerp`);
|
||
de-literalise `pluriwave_theme.dart:12,15`; create `lib/tema/pluriwave_typography.dart` built inside
|
||
`PluriWaveTheme.dark()`.
|
||
- [x] 1.4 RED — write `pluri_push_scaffold_test.dart`: each of the 5 root screens mounted bare builds zero
|
||
`Scaffold`; a `PluriPushScaffold` renders exactly one 56px `AppBar` + back affordance and exposes no
|
||
`bottomNavigationBar` parameter on its constructor.
|
||
- [x] 1.5 GREEN — implement `lib/widgets/pluri_push_scaffold.dart` per the ADR-2 API (`title`, `body`,
|
||
`titleOverride`, `leadingIcon`, `onBack`, `actions`, `bottom`, `floatingActionButton`, static `push`).
|
||
- [x] 1.6 RED — write `estado_navegacion_test.dart`: `irA()` transitions `RaizPluriWave`, no-ops and does not notify
|
||
on same-root, `indice` matches enum declaration order.
|
||
- [x] 1.7 GREEN — implement `lib/estado/estado_navegacion.dart` (`RaizPluriWave` enum, `EstadoNavegacionRaiz`);
|
||
register it in `app.dart`'s `MultiProvider`; convert the 3 `setState(() => _indice = ...)` alarm-deep-link
|
||
sites (`app.dart:310,333,350`) to `irA(...)`.
|
||
- [x] 1.8 RED — update `test/widget_test.dart` to expect "Escuchar" (not "Inicio") as the first tab's label, with
|
||
`PluriIconGlyph.home` unchanged as its icon.
|
||
- [x] 1.9 GREEN — change `app_en.arb` line 4 `navHome` value to `"Listen"` and `app_es.arb` line 4 to `"Escuchar"`;
|
||
retitle the first tab in `lib/app.dart`. Leave the other 11 ARB files untouched (WU18's job).
|
||
- [x] 1.10 REFACTOR — sweep `lib/tema/`, `app.dart` for now-dead literals; confirm zero consumer screens were
|
||
touched (WU1 converts zero screens to `PluriPushScaffold` — that is what keeps this commit at its line budget).
|
||
- [x] 1.11 Verify — run the command above; confirm `git diff --stat` touches only tokens/theme/typography files, the
|
||
new push-scaffold and nav-state files, `app.dart`, `app_en.arb`, `app_es.arb`, and their tests.
|
||
|
||
## WU2 — Android Auto verification (zero code)
|
||
|
||
**Commit**: `test(auto): confirm Android Auto tree matches the redesign, no code changes`
|
||
**Depends on**: —
|
||
**Spec refs**: `android-auto-media` — no requirement delta; this WU records the confirmation
|
||
**Verify**: `flutter test test/servicios/navegacion_auto_test.dart && git diff --stat lib/servicios/navegacion_auto.dart`
|
||
**Tests**: none created or modified — existing suite re-run only
|
||
|
||
- [x] 2.1 Run `flutter test test/servicios/navegacion_auto_test.dart`; confirm every case passes unmodified.
|
||
- [x] 2.2 Confirm `git diff lib/servicios/navegacion_auto.dart` is empty — no code change in this commit.
|
||
- [x] 2.3 Record in the commit body: A1-A5 closed, zero code changes, `MediaBrowserService` model confirmed
|
||
non-custom-renderable (the waveform in A4 is not implementable on this platform).
|
||
- [x] 2.4 No REFACTOR step — this WU is verification-only by design.
|
||
|
||
## WU3a — Settings shell + AUDIO/EMISORAS groups
|
||
|
||
**Commit**: `refactor(ajustes): split Settings AUDIO/EMISORAS into pushed detail screens`
|
||
**Depends on**: WU1
|
||
**Spec refs**: `app-navigation-shell` — Push-Chrome on Second-Level Screens (Settings detail screen scenario)
|
||
**Verify**: `flutter test test/pantallas/pantalla_ajustes_test.dart test/pantallas/ajustes/ && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: one per new screen under `test/pantallas/ajustes/` (7 files, AUDIO + EMISORAS)
|
||
**Modified tests**: `test/pantallas/pantalla_ajustes_test.dart` (near-total rewrite)
|
||
|
||
- [x] 3a.1 RED — rewrite `pantalla_ajustes_test.dart`: root renders exactly 4 grouped nav lists, zero inline
|
||
controls, file stays under 400 lines. **Corrected at apply time**: WU3a converts only AUDIO + EMISORAS (2
|
||
groups, 7 rows) per its own tasks 3a.4-3a.6 and the explicit "WU3a delivers 7 of 12 screens" scope — the
|
||
remaining 5 sections (GRABACIONES Y MÚSICA, APLICACIÓN) stay inline pending WU3b. "4 groups / <400 lines" is
|
||
the combined WU3a+WU3b end state (matches design ADR-3's own aggregate blast-radius note), not a WU3a-only
|
||
claim; the rewritten test asserts the true WU3a-scoped state instead (2 groups zero-inline, 5 sections still
|
||
inline and reachable).
|
||
- [x] 3a.2 RED — write one test per AUDIO/EMISORAS detail screen (Ecualizador, Salida de audio, Temporizador de
|
||
sueño, Grupos de favoritos, Emisora preferida, Emisoras personalizadas, Orden de listas) asserting it renders
|
||
inside a `PluriPushScaffold` and its moved controls still respond.
|
||
- [x] 3a.3 GREEN — create `lib/pantallas/ajustes/widgets/fila_ajuste.dart` (`FilaAjuste` / `GrupoAjustes` primitives).
|
||
- [x] 3a.4 GREEN — cut `_SeccionEcualizador` (694-782), `_SeccionEcualizadorAvanzado` + `_FilaDispositivo` +
|
||
`_DialogoEdicionDispositivo` (782-1013), `_SeccionTimerSueno` + `_FormularioDuracionTimer` (399-604) into
|
||
`pantalla_ajustes_ecualizador.dart`, `pantalla_ajustes_salida_audio.dart`, `pantalla_ajustes_timer_sueno.dart`.
|
||
Verbatim-move rule: delete only the panel header row; body is untouched. The two sections whose header row
|
||
carried a real action (not just a status readout) — Temporizador de sueño's "Add" and, in 3a.5, Grupos de
|
||
favoritos' "Add list" / Emisoras personalizadas' "Add" — keep that action in the body, right-aligned, rather
|
||
than dropping it; `_SeccionEcualizador`'s header also carried a status `Chip`, dropped since the very next row
|
||
(the enable switch) already shows the same state.
|
||
- [x] 3a.5 GREEN — cut `_SeccionGruposFavoritos` (1187), `_SeccionEmisoraPreferida` (1342), `_SeccionEmisoras` +
|
||
`_FormularioEmisora` (1459-1669), `_SeccionOrdenListas` (1133) into `pantalla_ajustes_grupos_favoritos.dart`,
|
||
`pantalla_ajustes_emisora_preferida.dart`, `pantalla_ajustes_emisoras_personalizadas.dart`,
|
||
`pantalla_ajustes_orden_listas.dart`.
|
||
- [x] 3a.6 GREEN — in the root, replace the 7 moved sections with `FilaAjuste` rows under two `GrupoAjustes` cards
|
||
(AUDIO, EMISORAS); each row pushes its screen via `PluriPushScaffold.push`. Two new ARB keys added (en/es only,
|
||
matching the WU1 precedent) for the group eyebrow labels: `settingsGroupAudioTitle` ("AUDIO"/"AUDIO"),
|
||
`settingsGroupStationsTitle` ("STATIONS"/"EMISORAS") — all 7 detail-screen titles reuse existing ARB keys
|
||
(the same string the old in-body header already showed), so no other new UI copy was introduced.
|
||
- [x] 3a.7 REFACTOR — confirm zero business-logic edits inside any `_CuerpoX` (diff reads "header removed, body
|
||
identical"); confirm `app.dart`'s import of `pantalla_ajustes.dart` is unchanged.
|
||
- [x] 3a.8 Verify — `git diff` touches only screen files (no service/state file) — confirmed. Root file line count:
|
||
**788 lines, not yet <400** — accurate for WU3a alone (5 sections remain inline; see 3a.1's note). WU3b's own
|
||
3b.5 REFACTOR is where the root actually crosses under 400.
|
||
|
||
**`size:exception` — "move-only diff".** ~800-1000 changed lines (design-verified figure), ~85% relocated not
|
||
modified. Do not attempt to slice under 450.
|
||
|
||
## WU3b — Settings GRABACIONES Y MÚSICA / APLICACIÓN groups
|
||
|
||
**Commit**: `refactor(ajustes): split remaining Settings sections into pushed screens`
|
||
**Depends on**: WU3a
|
||
**Spec refs**: `app-navigation-shell` — Push-Chrome on Second-Level Screens
|
||
**Verify**: `flutter test test/pantallas/pantalla_ajustes_test.dart test/pantallas/ajustes/ && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: one per new screen under `test/pantallas/ajustes/` (5 files: Grabaciones, Música local, Idioma,
|
||
Backup, Info)
|
||
**Modified tests**: `test/pantallas/pantalla_ajustes_test.dart` (extended)
|
||
|
||
- [x] 3b.1 RED — extend detail-screen tests for the remaining 5 sections, asserting `PluriPushScaffold` usage and
|
||
preserved controls. **Applied against the CURRENT 788-line file (re-read fresh at apply time, not the stale
|
||
92-1896 line references below, which were computed against the pre-WU3a 1,897-line file).** Root test file:
|
||
removed the now-false "Sections pending WU3b remain inline" and "Phase 7" groups, added a `WU3b — GRABACIONES Y
|
||
MÚSICA and APLICACIÓN groups` group (5 scenarios: 2-row/3-row zero-inline-control checks, 2 push-navigation
|
||
checks, one "root is exactly 4 GrupoAjustes cards" check). The 2 relocated Phase-7 friendly-folder-name
|
||
scenarios moved verbatim into the new `ajustes/pantalla_ajustes_musica_local_test.dart`, targeting the isolated
|
||
pushed screen directly instead of scrolling to find it inside the whole root.
|
||
- [x] 3b.2 GREEN — cut `_SeccionGrabaciones`, `_SeccionMusicaLocal` (Stateful) into `pantalla_ajustes_grabaciones.dart`,
|
||
`pantalla_ajustes_musica_local.dart` — verbatim, header row removed per the established rule.
|
||
- [x] 3b.3 GREEN — cut `_SeccionIdioma` + `_IdiomaDisponible`, `_SeccionBackup`, `_SeccionInfo` into
|
||
`pantalla_ajustes_idioma.dart`, `pantalla_ajustes_backup.dart`, `pantalla_ajustes_info.dart`. **Correction
|
||
found at apply time**: unlike the other 4 sections, `_SeccionInfo` never had its own header icon+title row —
|
||
its first tile (app name + version) already served that role — so there was no header to strip for that one
|
||
screen; its body moved in full, unchanged. This is also why `infoSectionTitle` is a genuinely new ARB key (no
|
||
existing in-body header string covered a bare "Info" label), unlike the other 4 screens which all reuse
|
||
pre-existing strings.
|
||
- [x] 3b.4 GREEN — in the root, replaced the remaining 5 sections with `FilaAjuste` rows under two more
|
||
`GrupoAjustes` cards: RECORDINGS & MUSIC (Recordings, Local music) and APPLICATION (Language, Backup, Info).
|
||
Confirmed sleep timer (WU3a) and backup/restore (this WU) rows are both present — corollary ruling, nothing
|
||
dropped. **3 new ARB keys** (en/es only, matching WU1/WU3a's precedent of leaving the other 11 locales for
|
||
WU18): `settingsGroupRecordingsTitle` ("RECORDINGS & MUSIC"/"GRABACIONES Y MÚSICA"),
|
||
`settingsGroupApplicationTitle` ("APPLICATION"/"APLICACIÓN") for the two new group eyebrow labels, and
|
||
`infoSectionTitle` ("Info"/"Información") for the one row with no pre-existing header string (see 3b.3). All
|
||
other row titles reuse existing ARB keys.
|
||
- [x] 3b.5 REFACTOR — confirmed `pantalla_ajustes.dart` now contains exactly 4 `GrupoAjustes` cards and is **198
|
||
lines** (well under the 400-line target); deleted all 5 now-unused private section widgets plus every import
|
||
that was only needed by their bodies (`dart:async`, `dart:io`, `file_picker`, `package_info_plus`,
|
||
`path_provider`, `provider`, `share_plus`, `estado_grabacion.dart`, `estado_idioma.dart`, `estado_radio.dart`,
|
||
`musica_local_auto.dart`, `pluri_glass_surface.dart`, `pluri_onboarding_dialog.dart` — none of these are read
|
||
by the root anymore since it is pure navigation chrome).
|
||
- [x] 3b.6 Verify — scoped suite (`pantalla_ajustes_test.dart` + `ajustes/`) green: 45/45. All 12 detail screens
|
||
present and reachable. `git diff` touches only screen/test/ARB(+generated l10n) files — no service/state file
|
||
touched. Full suite: 592/592 (up from 579). `flutter analyze`: 1 issue, identical to baseline.
|
||
|
||
**`size:exception` — "move-only diff".** Realized: **2,190 changed lines** (1,485+ / 705-) across 28 files — under
|
||
the ~2,500-3,200 estimate this time (Música local's compact Stateful body and this batch's conservative choice to
|
||
verify reachability rather than tap through unmocked native-channel controls for Backup/Grabaciones' riskiest rows
|
||
both kept it below forecast). Same "move-only diff" justification as WU3a.
|
||
|
||
## WU4 — Favoritos restyle
|
||
|
||
**Commit**: `feat(favoritos): replace stacked group panels with chip-filtered reorderable list`
|
||
**Depends on**: WU1
|
||
**Spec refs**: `favorites-organization` — Chip-Filtered Flat List, Drag-to-Reorder Within the Active Filter, Sort
|
||
Action Using Existing Criteria, Group Management Reachable from Favoritos, Custom-Station CTA Preserved
|
||
**Verify**: `flutter test test/pantallas/pantalla_favoritos_test.dart test/pantallas/pantalla_favoritos_plural_test.dart test/pantallas/ajustes/pantalla_ajustes_emisoras_personalizadas_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --cached --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `test/pantallas/pantalla_favoritos_test.dart` (see 4.1's note — this WU's actual new test file, not
|
||
`pantalla_favoritos_plural_test.dart`).
|
||
**Modified tests**: `test/pantallas/ajustes/pantalla_ajustes_emisoras_personalizadas_test.dart` (comment only, no
|
||
scenario change — see 4.4's note).
|
||
|
||
- [x] 4.1 RED — **corrected at apply time**: `pantalla_favoritos_plural_test.dart` (the file this task originally
|
||
named) never imports `PantallaFavoritos` — it only exercises `stationCount`'s ARB plural formatting via
|
||
`AppLocalizations` directly, with no widget-level coverage of this screen at all. Left it untouched (still a
|
||
valid, unrelated regression guard) and created **`test/pantallas/pantalla_favoritos_test.dart`** instead, with:
|
||
3 state-layer tests for the new `EstadoRadio` surface (`listaFavoritosManual`, `reordenarFavorito`,
|
||
`ordenarFavoritos` — see 4.3's note) plus 6 widget-level scenarios (empty-state CTA, chip filter narrows the
|
||
list, drag-reorder persists across a simulated restart, `swap_vert` sort, create-group action + new-group-chip
|
||
reactivity, custom-station CTA).
|
||
- [x] 4.2 GREEN — replaced `_GrupoFavoritosPanel` with `_FilaChipsGrupos` (horizontally-scrollable
|
||
`ChoiceChip` row, "{name} · {count}" label, new ARB keys `favoriteGroupsChipLabel`/`favoritesFilterAllLabel`)
|
||
and a `ReorderableListView` (`header`/`footer` hold the screen header, chip row, sort action and the CTA;
|
||
`buildDefaultDragHandles: false` + `ReorderableDragStartListener` per row, matching the spec's "via a drag
|
||
handle" wording rather than whole-row long-press). Used the modern `onReorderItem` callback, not the
|
||
now-`@Deprecated` `onReorder` (Flutter 3.44 marks it obsolete — using it would have added a new
|
||
`deprecated_member_use` warning above the 1-issue baseline).
|
||
- [x] 4.3 GREEN — **design decision, not specified by any ADR (WU4 has none)**: `EstadoRadio.listaFavoritos` already
|
||
unconditionally re-sorts by the GLOBAL `ordenListas` setting on every read, which would silently discard any
|
||
drag-to-reorder the instant anything reloads favorites. Added `listaFavoritosManual` (a new memoized getter
|
||
returning the stored `orden`-column sequence, untouched by `ordenListas`) instead of changing `listaFavoritos`
|
||
itself — `navegacion_auto.dart`'s Android Auto tree and the future Escuchar grid (WU5) both read
|
||
`listaFavoritos` and are correctly unaffected by Favoritos' own manual order. Added `reordenarFavorito(uuid,
|
||
nuevoIndice)` (thin wrapper over the ALREADY-EXISTING `ServicioFavoritos.reordenar` + `FakeServicioFavoritos`
|
||
test double — both pre-dated this WU, unused until now) and `ordenarFavoritos(criterio)` (applies
|
||
`OrdenEmisoras` via the existing `ordenarEmisoras()` function, then PERSISTS the result as the new manual order
|
||
via the same `reordenar` primitive in a loop — chosen so the sort action's result also survives a restart,
|
||
consistent with the drag-reorder persistence contract, even though the spec's own sort scenario only asserts
|
||
the immediate re-render).
|
||
- [x] 4.4 GREEN — create-group action: an `ActionChip` ("Manage lists", new ARB key `favoriteGroupsManage`) at the
|
||
end of the chip row pushes the EXISTING `PantallaAjustesGruposFavoritos` (Settings' screen, reused rather than
|
||
duplicated) — satisfies "Group Management Reachable... in addition to its existing entry point in Settings" as
|
||
a second entry point to the SAME screen. Custom-station CTA: a new dashed-bordered card (`_DashedBorderPainter`,
|
||
a small self-contained `CustomPainter` — no dashed-border package was available or added) opens the add-station
|
||
form directly. That form (`_FormularioEmisora`) was **renamed to public `FormularioEmisoraPersonalizada`** in
|
||
`pantalla_ajustes_emisoras_personalizadas.dart` so both screens share the one form instead of duplicating it;
|
||
its one existing test file only referenced the old name in a comment, updated, no scenario changed. New ARB key
|
||
`customStationsAddCta` ("Add custom station" / "Añadir emisora personalizada", matching the spec's quoted
|
||
Spanish text exactly).
|
||
- [x] 4.5 REFACTOR — extracted `_FilaChipsGrupos`, `_FilaFavorito` (the reorderable row, now carrying a leading drag
|
||
handle plus the pre-existing assign/remove actions), `_CtaEmisoraPersonalizada`, `_DashedBorderPainter` as
|
||
private widgets. Confirmed `PantallaFavoritos` still constructs zero `Scaffold` (`pluri_push_scaffold_test.dart`
|
||
"The 5 root screens build zero Scaffold when mounted bare" still passes for it) — push-chrome is not applied
|
||
here, Favoritos keeps its tab bar.
|
||
- [x] 4.6 Verify — scoped suite green: 9/9 new + 2/2 `pantalla_favoritos_plural_test.dart` + 2/2
|
||
`pantalla_ajustes_emisoras_personalizadas_test.dart`. Reorder-persists scenario green (verified via a simulated
|
||
restart — `cargarFavoritos()` re-fetch from the fake service reflects the new order). Sort scenario green (both
|
||
state persistence and the visual re-render order). Full suite: 614/614 green (2 skipped, unchanged), up from
|
||
605. `flutter analyze`: 1 issue, identical to baseline (3 new warnings + 1 new info surfaced during REFACTOR —
|
||
unused import, 3 unused optional painter parameters, missing `super.key` on the newly-public form widget — all
|
||
fixed before this count).
|
||
|
||
## WU5 — Escuchar restructure
|
||
|
||
**Commit**: `feat(escuchar): replace discovery browser with embedded player and favorites grid`
|
||
**Depends on**: WU1, WU4
|
||
**Spec refs**: `app-navigation-shell` — Root-to-Root Switching Without Push
|
||
**Verify**: `flutter test test/pantallas/pantalla_inicio_test.dart test/pantallas/pantalla_inicio_rebuild_test.dart test/widgets/mini_reproductor_configurar_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --cached --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: `pantalla_inicio_test.dart`, `mini_reproductor_configurar_test.dart`, `test/helpers/fakes.dart`
|
||
(see 5.4's note). `pantalla_inicio_rebuild_test.dart` needed no change — it never taps "Ver todas", so
|
||
`EstadoNavegacionRaiz`'s absence there is never exercised (lazily read only inside the button's `onPressed`).
|
||
|
||
- [x] 5.1 RED — the ADR-7 anti-cache test: `await audio.reproducir(estacionB)` mutates the underlying
|
||
`FakeServicioAudio` **directly**, bypassing `EstadoRadio.reproducir()` entirely (the same shape as
|
||
`navegacion_auto.dart`'s out-of-band mutation); asserts the hero's rendered station name follows. The hero
|
||
being a `StatelessWidget` (no `State` class at all) makes "zero cached fields" true by construction — this
|
||
behavioral test is also what would catch a hypothetical cached-field regression, since a cached value set once
|
||
in `initState` would not follow an external mutation the way this test requires.
|
||
- [x] 5.2 RED — hazard test added to `mini_reproductor_configurar_test.dart`: with a station actively "reproduciendo"
|
||
(so a naive `emisora == null` check couldn't accidentally satisfy it), `MiniReproductor(visible: false)` renders
|
||
nothing (`find.text` for the station name finds nothing) while `configurarLocalizaciones` still ran exactly
|
||
once — confirming `didChangeDependencies` fired independent of `build()`'s early return.
|
||
- [x] 5.3 RED — asserts `navegacion.actual == RaizPluriWave.favoritos` after tapping "Ver todas", using a
|
||
`_RecordingNavigatorObserver` (counts `didPush` calls) to assert the push count is **unchanged** before/after
|
||
the tap — proves "switches tabs, does not push" mechanically rather than by inspection.
|
||
- [x] 5.4 RED — **test-file correction, noted explicitly**: `pantalla_inicio_rebuild_test.dart` needed no scenario
|
||
change (see the header note above). Discovered and fixed a genuine, pre-existing gap while writing 5.1: no
|
||
test in this codebase had ever exercised `ServicioAudio.androidAudioSessionIdStream` against a bare
|
||
`FakeServicioAudio` — the real getter requires `registrarHandler()` (`main.dart`, production-only) and throws
|
||
`"registrarHandler() no fue llamado en main.dart"` otherwise. `pantalla_reproductor.dart` has always read this
|
||
exact getter but has **no test file at all**, so the gap was latent until `PantallaInicio` started wiring
|
||
`VisualizadorAudio` to it here. Added a `Stream<int?>.empty()` override to `FakeServicioAudio`
|
||
(`test/helpers/fakes.dart`) — purely additive, matches `VisualizadorAudio`'s own documented no-native-session
|
||
fallback, does not change any existing test's behavior (grep-confirmed nothing else reads this getter).
|
||
`context.select` scoping: the hero selects `emisoraActual` itself (one scalar per ADR-7 rule 2 — `Emisora`'s
|
||
own `==`/`hashCode` are uuid-based, so this only rebuilds on a real station change, not on audio-buffer
|
||
notifications); the fast-changing `EstadoReproduccion` is read via `StreamBuilder` instead (the same pattern
|
||
`_Controles`/`MiniReproductor` already use), so playback-status ticks never even reach the hero's own rebuild
|
||
path. `pantalla_inicio_rebuild_test.dart`'s EQ-preset-doesn't-rebuild guard still passes unmodified.
|
||
- [x] 5.5 GREEN — built `_EscucharHero` (`StatelessWidget`) in `pantalla_inicio.dart`: square art (`_ArteEscuchar`,
|
||
`ClipRRect` not `ClipOval` — the full player's own `_WaveHero` stays circular and untouched), live/offline
|
||
`PluriStatusPill`, `VisualizadorAudio(barras: 30, altura: 26, color: liveGreen)`. Transport row
|
||
(`_FilaTransporteEscuchar`) is favorite / EQ toggle / stop / play-pause (primary) / sleep, in that order —
|
||
**design decision, not spec-tested** (no GIVEN/WHEN/THEN scenario enumerates the exact 5 actions; only ADR-7's
|
||
structural rules are): the first 4 mirror the full player's own existing app-bar + transport actions
|
||
(favorite, EQ, stop, play/pause) exactly, with sleep as the documented 5th, satisfying "no new playback
|
||
methods" (rule 3) by construction — every action calls an existing `EstadoRadio`/`EstadoEcualizador` method. A
|
||
separate "tool-tray entry chip" (`OutlinedButton.icon`) opens the full player via the existing
|
||
`PantallaReproductor.abrir`. 5 new ARB keys (en/es only, established precedent): `yourStationsTitle`,
|
||
`seeAllAction`, `openFullPlayerTooltip`, `nothingPlayingTitle`, `nothingPlayingSubtitle` (the last two back a
|
||
lightweight placeholder state when `emisoraActual` is null, so the hero degrades gracefully before any
|
||
playback has started).
|
||
- [x] 5.6 GREEN — added `MiniReproductor.visible` (default `true`) and `static const double altura`. **Measured, not
|
||
guessed**: added a self-verifying test asserting `altura` is `closeTo` (±4px tolerance) the REAL
|
||
`tester.getSize(find.byType(MiniReproductor)).height` with a representative station and default text scale;
|
||
ran it with a placeholder first, read the actual measured value (`72.0`) from the assertion failure, then set
|
||
the constant to match exactly. `build()` returns `SizedBox.shrink()` when `!visible`, independent of
|
||
`didChangeDependencies` (task 5.2's hazard test proves the side effect still runs). `app.dart` passes
|
||
`visible: indice != RaizPluriWave.escuchar.index`.
|
||
- [x] 5.7 GREEN — added `PluriLayout.escucharBottomChromeInset = bottomChromeInset - MiniReproductor.altura` and
|
||
wired it into `pantalla_inicio.dart`'s own bottom `SliverPadding` (replacing the plain `bottomChromeInset`
|
||
every other root/scrollable still uses) — the mini player is hidden for this whole screen, not just the new
|
||
sections, so the reduced inset applies to the still-present discovery grid too.
|
||
- [x] 5.8 GREEN — added `_seccionTusEmisoras` (replaces the removed `_heroHeader`'s call site, right after the hero):
|
||
`listaFavoritos` (not `listaFavoritosManual` — Escuchar previews the same globally-ordered list every other
|
||
screen shows, it doesn't need Favoritos' own manual-order view) capped at 6, horizontally-scrollable compact
|
||
`TarjetaEmisora` cards, "Ver todas" (`TextButton`) calling
|
||
`context.read<EstadoNavegacionRaiz>().irA(RaizPluriWave.favoritos)`. Empty-favorites case reuses the existing
|
||
`favoritesEmptySubtitle` string rather than a new key.
|
||
- [x] 5.9 REFACTOR — confirmed `_seccionCercanas`/`_seccionTendencias`/`_chipGeneros`/`_errorBanner`/`_gridEmisoras`
|
||
(the discovery grid itself) are untouched, left in place below the new hero + "Tus emisoras" section, exactly
|
||
as instructed — WU6 relocates and deletes them. Confirmed `flutter test/widgets/pluri_push_scaffold_test.dart`
|
||
"The 5 root screens build zero Scaffold when mounted bare PantallaInicio" still passes (the hero adds slivers,
|
||
no `Scaffold`).
|
||
- [x] 5.10 Verify — anti-cache test green; mini-player-hidden-but-side-effect-ran test green; tab-switch-not-push
|
||
test green (push count unchanged); `git diff --stat` confirmed empty for `lib/widgets/visualizador_audio.dart`
|
||
AND `lib/estado/estado_radio.dart` (WU5 touches neither). Full suite: 618/618 green (2 skipped, unchanged), up
|
||
from 614. `flutter analyze`: 1 issue, identical to baseline.
|
||
|
||
**Discovery worth flagging for WU6+ (or any future WU rendering an actively-playing station in a widget test):**
|
||
`VisualizadorAudio` starts an indeterminately-**repeating** `AnimationController` (`visualizador_audio.dart:77`,
|
||
`_controller.repeat()`) whenever the stream reports "reproduciendo"/"cargando"/"reconectando" — this is the same
|
||
class of hazard as an indeterminate spinner (`pumpAndSettle()` never returns while it keeps scheduling frames), just
|
||
via an animation instead of a progress indicator. Any test that renders a playing station through a widget that
|
||
embeds `VisualizadorAudio` (this hero, the full player) must use a **bounded** `pump()`, never `pumpAndSettle()`,
|
||
once the station starts "reproduciendo".
|
||
|
||
## WU6 — Buscar landing state + filters
|
||
|
||
**Commit**: `feat(buscar): add discovery landing state, filter pills, counter, and sort`
|
||
**Depends on**: WU5
|
||
**Spec refs**: `station-discovery-browse` — Buscar Landing State Shows Discovery Content, Active-Filter Pills and
|
||
Results Counter, One-Tap Clear-All-Filters on Empty Results, Client-Side Search Sort Only
|
||
**Verify**: `flutter test test/pantallas/pantalla_buscar_shimmer_test.dart test/estado/estado_busqueda_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: `pantalla_buscar_shimmer_test.dart`, `estado_busqueda_test.dart` (non-sort paths unmodified)
|
||
|
||
- [x] 6.1 No spike needed here — the sort ruling is already verified (Engram 2505): server-side `order`/`reverse`
|
||
exist but are rejected; `OrdenEmisoras` is the only extension point. Do not re-derive.
|
||
- [x] 6.2 RED — empty query renders discovery content (near-you / genres / trending / Países entry); non-empty query
|
||
replaces it with the results view.
|
||
- [x] 6.3 RED — active-filter pills (country/language/quality) with a close affordance; results counter text; the
|
||
one-tap "Quitar los 2 filtros" action on a 2-filter/0-result fixture.
|
||
- [x] 6.4 RED — regression guard: every rendered "Ordenar" option maps to a real `OrdenEmisoras` case with its own
|
||
test.
|
||
- [x] 6.5 GREEN — move the discovery content (near-you/genres/trending, Países entry point) from
|
||
`pantalla_inicio.dart`'s old sections into `pantalla_buscar.dart`'s empty-query landing state; **delete** the
|
||
moved sections from `pantalla_inicio.dart` now (completes WU5's deferred cleanup, task 5.9).
|
||
- [x] 6.6 GREEN — collapse the 3 always-visible `FilterChip` rows into an active-pills bar with bottom-sheet
|
||
pickers; add the results counter and clear-all-filters action.
|
||
- [x] 6.7 GREEN — extend `enum OrdenEmisoras` (`orden_emisoras.dart:4`) only with criteria backed by existing
|
||
`Emisora` fields (`bitrate`, `votes`, `clickcount`); wire "Ordenar" client-side only — no `order`/`reverse`
|
||
param added to any request.
|
||
- [x] 6.8 REFACTOR — confirm `estado_busqueda_test.dart`'s non-sort paths still pass unmodified.
|
||
- [x] 6.9 Verify — no test in this WU asserts an `order` param on any outgoing request; sort-option regression guard
|
||
green.
|
||
|
||
## WU7 — Países browser + `_get` transport extraction
|
||
|
||
**Commit**: `feat(paises): add country browser and extract shared radio transport`
|
||
**Depends on**: WU6
|
||
**Spec refs**: `station-discovery-browse` — Países Browser Over the Verified Countries Contract, Existing Station
|
||
Calls Unchanged by Transport Extraction
|
||
**Verify**: `flutter test test/servicios/servicio_radio_test.dart test/servicios/servicio_radio_transporte_test.dart test/estado/estado_busqueda_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `test/servicios/servicio_radio_transporte_test.dart`, country-model test, `pantalla_paises_test.dart`
|
||
**Modified tests**: `estado_busqueda_test.dart` (additions only); `servicio_radio_test.dart` is **not modified**
|
||
|
||
> **Special ordering rule (non-negotiable): task 7.1 must be green before task 7.4 begins.** Its shuffled-fixture
|
||
> UUID-sequence assertion is what makes the extraction safe — skipping it lets a sort silently sink into transport.
|
||
|
||
- [x] 7.1 RED→GREEN (characterisation, green by construction against the **unmodified** `_get`) — create
|
||
`servicio_radio_transporte_test.dart` pinning, for each of the 7 `_get`-based methods (`obtenerPopulares`,
|
||
`obtenerTendencias`, `buscarPorNombre`, `buscarPorPais`, `buscarPorIdioma`, `buscarPorTag`, `buscar`): request
|
||
path, `lastcheckok=1` present, `hidebroken=true` present, non-empty `User-Agent`, exact
|
||
`order`/`reverse`/`limit`/`offset`, and the exact returned UUID sequence from a fixture with deliberately
|
||
shuffled `bitrate`/`clickcount`/`votes`. Pin `registrarClick`'s path and that it sends *some* `User-Agent`
|
||
(do **not** pin its known-stale literal at line 325 — that would convert a bug into a contract).
|
||
- [x] 7.2 RED — `PaisRadio.fromApi` parses `stationcount` via `int.tryParse` from a fixture where it arrives as a
|
||
JSON **string**; defaults safely on missing fields.
|
||
- [x] 7.3 RED — `estado_busqueda_test.dart` additions for `paises`, `cargandoPaises`, `cargarPaises()` (in-memory
|
||
cache guard); `pantalla_paises_test.dart` for "Tus idiomas" + full alphabetical list with counts.
|
||
- [x] 7.4 GREEN — extract `_getJson` (transport only: `_descubrirServidores`, host rotation, bounded retries,
|
||
User-Agent, timeout, status check, `json.decode`, sticky-host bookkeeping) out of `_get`, moved as **one
|
||
block, no logic edits**; `_get` re-implemented on top, still applying `lastcheckok`, `Emisora.fromApi`,
|
||
`_compararCalidad`.
|
||
- [x] 7.5 GREEN — implement `lib/modelos/pais_radio.dart`, `obtenerPaises()` (calls
|
||
`_getJson('/json/countries', const {})`, no `lastcheckok`, no `order`), and `lib/pantallas/pantalla_paises.dart`.
|
||
- [x] 7.6 RE-RUN (proof step) — re-run task 7.1's tests against the extracted code; must be byte-identical green,
|
||
including the shuffled-fixture UUID-sequence assertion.
|
||
- [x] 7.7 REFACTOR — document in the commit body that a successful `/json/countries` call now warms
|
||
`_servidorActual` for subsequent station calls — an accepted, intentional behaviour delta, not a regression.
|
||
- [x] 7.8 Verify — grep confirms `countrycodes` appears nowhere in `lib/`; the string-`stationcount` fixture renders
|
||
`482` without a cast error; the countries request has no `lastcheckok`; all 8 original methods' tests pass
|
||
identically before and after.
|
||
|
||
## WU8 — Alarmas root restyle
|
||
|
||
**Commit**: `feat(alarmas): simplify alarm cards and add vacation summary row`
|
||
**Depends on**: WU1
|
||
**Spec refs**: `native-alarms` (delta) — Alarm Card Tap-to-Edit Swipe-to-Delete, Hero Banner Inline Skip;
|
||
`alarm-vacation-ranges` — Vacation Summary Row Replaces the Inline Panel
|
||
**Verify**: `flutter test test/pantallas/pantalla_alarmas_editor_test.dart test/estado/estado_alarmas_test.dart test/estado/estado_alarmas_snooze_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: `pantalla_alarmas_editor_test.dart` (+ new card-gesture scenarios); `estado_alarmas_test.dart`,
|
||
`estado_alarmas_snooze_test.dart` must pass unmodified
|
||
|
||
- [ ] 8.1 RED — tapping a simplified alarm card (giant time + station + switch) opens the editor pre-filled;
|
||
swiping triggers delete with confirmation; no capability from the old always-visible button row is lost.
|
||
- [ ] 8.2 RED — the hero banner's inline "Saltar" action skips the featured (soonest-firing) alarm, consistent with
|
||
existing skip-next behaviour.
|
||
- [ ] 8.3 RED — the vacation panel is now a summary row (range count + next-range countdown) that pushes a screen
|
||
on tap (destination screen not yet built — assert `PluriPushScaffold.push` is invoked; WU9 builds it).
|
||
- [ ] 8.4 GREEN — simplify `_TarjetaAlarma` to giant time/name/switch; move Edit/Skip/Delete behind tap/swipe.
|
||
- [ ] 8.5 GREEN — add the hero banner's inline "Saltar" pill calling the existing skip-next path.
|
||
- [ ] 8.6 GREEN — replace `_PanelVacaciones`'s inline list with a summary row wired to push the Vacaciones manager
|
||
screen.
|
||
- [ ] 8.7 REFACTOR — confirm no scheduling/dismiss-guard code path was touched; `estado_alarmas_test.dart` and
|
||
`estado_alarmas_snooze_test.dart` pass unmodified.
|
||
- [ ] 8.8 Verify — tap/swipe/hero-skip scenarios all green; confirm
|
||
`pantalla_alarma_sonando_dismiss_guard_test.dart` was not touched by this WU (it only edits the root list
|
||
screen).
|
||
|
||
## WU9 — Vacaciones manager (new screen)
|
||
|
||
**Commit**: `feat(vacaciones): add vacation range manager screen`
|
||
**Depends on**: WU8
|
||
**Spec refs**: `alarm-vacation-ranges` — Active Range Detection, Per-Alarm Pause-Impact Computation, Upcoming Ranges
|
||
Query, Past-Ranges History Query, Vacation Summary Row Replaces the Inline Panel (destination screen)
|
||
**Verify**: `flutter test test/estado/estado_alarmas_test.dart test/pantallas/pantalla_vacaciones_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `pantalla_vacaciones_test.dart`
|
||
**Modified tests**: `estado_alarmas_test.dart` (additions only)
|
||
|
||
- [ ] 9.1 RED — with a **fixed `ahora`**: `rangoVacacionesActivo` returns the active range + days-remaining, or null
|
||
when none matches; `vacacionesProximas` returns future ranges ascending by start; `vacacionesPasadas` returns
|
||
ended ranges descending by end, disjoint from active/upcoming.
|
||
- [ ] 9.2 RED — `impactoDeRango`: given 3 alarms (2 with `sonarEnVacaciones == false`, 1 with `true`) during an
|
||
active range, assert exactly those 2 in `pausadas` and that 1 in `noAfectadas`, mirroring
|
||
`servicio_programacion_alarmas.dart:150`'s predicate exactly.
|
||
- [ ] 9.3 RED — `pantalla_vacaciones_test.dart`: active-range hero with progress + per-alarm impact line;
|
||
"PROGRAMADOS" upcoming list; dashed "Añadir rango" CTA; "Rangos pasados" history row/screen.
|
||
- [ ] 9.4 GREEN — add the 4 pure query members to `EstadoAlarmas` (`{DateTime? ahora}` defaulting to
|
||
`DateTime.now()`), delegating `rangoVacacionesActivo` to the existing `RangoVacaciones.contiene(fecha)`.
|
||
- [ ] 9.5 GREEN — add `ImpactoVacaciones` beside `RangoVacaciones` in `lib/modelos/alarma_musical.dart`.
|
||
- [ ] 9.6 GREEN — build `lib/pantallas/pantalla_vacaciones.dart` as a `PluriPushScaffold`; wire WU8's summary-row
|
||
tap to it.
|
||
- [ ] 9.7 REFACTOR — confirm none of the 4 new methods call `guardarVacaciones` or any reprogramming path
|
||
(read-only over `_alarmas`/`_vacaciones`); confirm no ticker/periodic timer was added.
|
||
- [ ] 9.8 Verify — `pantalla_alarma_sonando_dismiss_guard_test.dart` passes **unmodified** (hard rule — if a query
|
||
addition appears to require touching it, STOP and escalate; do not edit the test).
|
||
|
||
## WU10 — Alarm editor sheet rewrite
|
||
|
||
**Commit**: `feat(alarmas): rewrite alarm editor with inline time widget`
|
||
**Depends on**: WU8
|
||
**Spec refs**: `native-alarms` (delta) — Alarm Editor Preserves Date, Fallback Station, and Sound Fields
|
||
**Verify**: `flutter test test/widgets/editor_hora_inline_test.dart test/pantallas/pantalla_alarmas_fecha_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `test/widgets/editor_hora_inline_test.dart` (standalone)
|
||
**Modified tests**: `pantalla_alarmas_fecha_test.dart`
|
||
|
||
- [ ] 10.1 RED — `editor_hora_inline_test.dart`, **standalone, independent of the sheet**: drag/tap adjusts hour and
|
||
minute, wraps at 23:59→00:00, exposes correct a11y labels.
|
||
- [ ] 10.2 GREEN — implement `lib/widgets/editor_hora_inline.dart` (giant inline HH:MM display, drag/tap-to-adjust).
|
||
- [ ] 10.3 RED — update `pantalla_alarmas_fecha_test.dart`: one-time date alarm still creatable via the (now
|
||
collapsed) Advanced section; fallback-station picker and sound dropdown still settable and persisted.
|
||
- [ ] 10.4 GREEN — replace the native `showTimePicker` dialog with `EditorHoraInline`; keep weekday circles always
|
||
visible; move the date field, fallback-station picker, and sound dropdown into a collapsed "Advanced"
|
||
section rather than dropping them.
|
||
- [ ] 10.5 GREEN — restyle the volume/fade-in sliders (cosmetic only, no behaviour change).
|
||
- [ ] 10.6 REFACTOR — confirm the dismiss-guard test is untouched by this WU (editor sheet only, not the ringing
|
||
screen).
|
||
- [ ] 10.7 Verify — standalone widget test green independent of the sheet; date/fallback/sound round-trip test
|
||
green.
|
||
|
||
**`size:exception` recommended.** ~500-650 lines — the largest genuinely-new widget in the plan; already isolated
|
||
as its own unit per the proposal ("never bundled"). See forecast table footnote ‡.
|
||
|
||
## WU11 — Alarm ringing restyle
|
||
|
||
**Commit**: `feat(alarma-sonando): restyle ringing screen, drop live countdown label`
|
||
**Depends on**: WU1
|
||
**Spec refs**: `native-alarms` (delta) — Ringing Screen Shows a Static Status Label, Dismiss Guard and Force-Stop
|
||
Banner Survive the Restyle
|
||
**Verify**: `flutter test test/pantallas/pantalla_alarma_sonando_test.dart test/pantallas/pantalla_alarma_sonando_scaffold_test.dart test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: `pantalla_alarma_sonando_test.dart`, `pantalla_alarma_sonando_scaffold_test.dart`.
|
||
**`pantalla_alarma_sonando_dismiss_guard_test.dart` is run but MUST NOT be edited.**
|
||
|
||
> **Hard rule, restated:** if the restyle appears to require changing the dismiss-guard test, the task is to STOP
|
||
> and escalate — not to edit the test.
|
||
|
||
- [ ] 11.1 RED — update the two non-guard test files for the full-bleed blurred-art layout, giant 88px time
|
||
(`heroTime`, wrapped in `FittedBox(fit: BoxFit.scaleDown)` per the text-scaling rule), 3 fixed snooze tiles
|
||
(3/5/10 min, 10 highlighted), full-width stop pill.
|
||
- [ ] 11.2 RED — the status label renders "Subiendo volumen" (or localized equivalent) with no accompanying
|
||
numeric value that changes over time.
|
||
- [ ] 11.3 GREEN — restyle to a full-bleed art background; replace the `Wrap` of snooze chips with 3 fixed tiles;
|
||
replace the glass-card container.
|
||
- [ ] 11.4 GREEN — keep the status label static, sourced only from `widget.alarma` (unchanged from the existing
|
||
documented constraint); preserve the force-stop retry banner in the new layout.
|
||
- [ ] 11.5 REFACTOR — run `pantalla_alarma_sonando_dismiss_guard_test.dart` **unchanged**; if it fails, STOP — do
|
||
not edit it, escalate instead.
|
||
- [ ] 11.6 Verify — force-stop banner still appears on a simulated stop failure; dismiss-guard test file diff is
|
||
empty.
|
||
|
||
## WU13 — Ecualizador settings screen (5-band restyle)
|
||
|
||
**Commit**: `feat(eq): restyle equalizer screen and add custom presets`
|
||
**Depends on**: WU3a
|
||
**Spec refs**: `eq-custom-presets` — Five-Band Equalizer (Regression Guard), Custom Preset Save, Custom Preset
|
||
Naming Validates Non-Empty Input, Per-Station EQ Entry Point (component only; consumption is WU14),
|
||
Stations-With-Own-EQ Drill-Down, Base-vs-Per-Station Explainer Preserved, Active Output Surfaced on the Main Screen
|
||
**Verify**: `flutter test test/widgets/ecualizador_widget_test.dart test/servicios/servicio_presets_personalizados_test.dart test/servicios/servicio_ecualizador_test.dart test/estado/estado_ecualizador_test.dart test/servicios/servicio_audio_eq_reapply_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `servicio_presets_personalizados_test.dart`, `pantalla_ajustes_ecualizador_test.dart`
|
||
**Modified tests**: `ecualizador_widget_test.dart`. `servicio_ecualizador_test.dart`, `estado_ecualizador_test.dart`,
|
||
`servicio_audio_eq_reapply_test.dart` must pass **unmodified**.
|
||
|
||
> **Hazard, restated: persistence for custom presets must NOT go in `servicio_ecualizador.dart`.** That file has an
|
||
> empty-`git diff` success criterion.
|
||
|
||
- [ ] 13.1 RED — the Ecualizador screen renders exactly **5** vertical sliders (regression guard — reject any
|
||
future 7-slider change on sight).
|
||
- [ ] 13.2 RED — `servicio_presets_personalizados_test.dart`: save/list/delete round-trip for a named custom preset
|
||
via its **own** SharedPreferences key `eq_custom_presets_v1`.
|
||
- [ ] 13.3 RED — submitting an empty/whitespace-only preset name shows a validation message and persists nothing.
|
||
- [ ] 13.4 RED — the "Emisoras con ajuste propio" drill-down lists exactly the stations present in
|
||
`presetsPorEmisora`.
|
||
- [ ] 13.5 RED — the "Salida activa" row is visible on the main screen (not only Advanced) and updates on a
|
||
simulated device change; the base-vs-per-station explainer banner is visible.
|
||
- [ ] 13.6 GREEN — create `lib/servicios/servicio_presets_personalizados.dart` (**new file**, own SharedPreferences
|
||
key). **Do NOT add any member to `servicio_ecualizador.dart`.** `lib/modelos/preset_ecualizador.dart` stays
|
||
unchanged — a user preset is a `PresetEcualizador` with a user-supplied `nombre`; `toJson`/`desdeJson`
|
||
already exist.
|
||
- [ ] 13.7 GREEN — extend `EstadoEcualizador` with `presetsPersonalizados`, `guardarPresetPersonalizado(nombre)`,
|
||
`eliminarPresetPersonalizado(nombre)`, reading/writing only through the new service.
|
||
- [ ] 13.8 GREEN — restyle `lib/widgets/ecualizador_widget.dart` in place: strip the internal title + preset chip
|
||
row (lines 59-77 — the pushed header now carries the title), restyle the 5 `Card`/`RotatedBox`/`Slider`
|
||
tiles to the vertical-track look, add the `habilitado` parameter. Keep the `for (int i = 0; i < 5; i++)`
|
||
loop bound literally `5`.
|
||
- [ ] 13.9 GREEN — build `lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart` wiring
|
||
`EstadoEcualizador.presetPrincipal` / `cambiarPresetPrincipal`, surfacing "Salida activa" and the drill-down
|
||
row.
|
||
- [ ] 13.10 REFACTOR — reviewer check: confirm `git diff` is **empty** for `lib/servicios/servicio_ecualizador.dart`,
|
||
`lib/modelos/preset_ecualizador.dart`, and the band-application block at
|
||
`lib/servicios/servicio_audio.dart:749-762`.
|
||
- [ ] 13.11 Verify — the three EQ test files pass **unmodified**; exactly 5 sliders render.
|
||
|
||
## WU14 — Reproductor completo + per-station EQ sheet
|
||
|
||
**Commit**: `feat(reproductor): restructure full player with tool-tray and EQ sheet`
|
||
**Depends on**: WU13
|
||
**Spec refs**: `eq-custom-presets` — Per-Station EQ Entry Point From the Player; `multi-device-eq` (delta) —
|
||
Per-Station EQ Entry Relocates, Resolution Logic Does Not
|
||
**Verify**: `flutter test test/pantallas/pantalla_reproductor_test.dart test/servicios/servicio_ecualizador_test.dart test/estado/estado_ecualizador_test.dart test/servicios/servicio_audio_eq_reapply_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: `pantalla_reproductor_test.dart` (or equivalent widget test). Three EQ test files must pass
|
||
**unmodified**.
|
||
|
||
- [ ] 14.1 RED — the per-station EQ bottom sheet renders **the same `EcualizadorWidget` type** WU13 restyled
|
||
(assert by `runtimeType`, so a duplicate implementation fails the test, not just a visual review).
|
||
- [ ] 14.2 RED — the 4 tool-tray tiles ("EQ propio", "Grabar", sleep-timer value, "Compartir") each open their own
|
||
bottom sheet.
|
||
- [ ] 14.3 RED — opening "EQ propio" for a playing station shows 5 sliders bound to that station's resolved preset,
|
||
and a change round-trips through the existing per-station persistence path (`presetsPorEmisora` /
|
||
`presetsMatriz`).
|
||
- [ ] 14.4 GREEN — restructure `pantalla_reproductor.dart`: square art, favorite moved into the transport row,
|
||
single subtitle line (collapse the current `_InfoChips` `Wrap`), 4-tile tool-tray grid replacing the
|
||
always-expanded recording panel + separate sleep-timer button + EQ toggle.
|
||
- [ ] 14.5 GREEN — wire "EQ propio" to a bottom sheet hosting `EcualizadorWidget` bound via
|
||
`EstadoEcualizador.presetParaEmisora(uuid)` / `guardarPresetPorEmisora(uuid, ...)`.
|
||
- [ ] 14.6 GREEN — add the quality row + "Cambiar" action and the "Compartir" tool-tray tile.
|
||
- [ ] 14.7 REFACTOR — confirm no second EQ editor file was created; confirm the `multi-device-eq` regression
|
||
scenarios (device-event resolve-and-apply, first-seen bootstrap, cold start, connect/disconnect/reconnect,
|
||
toggle-off) still pass unmodified.
|
||
- [ ] 14.8 Verify — the 3 EQ test files remain green and unmodified; `EcualizadorWidget` type-identity assertion
|
||
passes.
|
||
|
||
## WU15 — Grabaciones library (new list)
|
||
|
||
**Commit**: `feat(grabaciones): add recordings library screen`
|
||
**Depends on**: WU3b
|
||
**Spec refs**: `recordings-library` — Storage Usage Display, Browsable Recordings List, Per-Recording Menu
|
||
Constrained to Rename/Share/Delete
|
||
**Verify**: `flutter test test/pantallas/pantalla_grabaciones_test.dart test/servicios/servicio_grabacion_radio_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `pantalla_grabaciones_test.dart`
|
||
**Modified tests**: `servicio_grabacion_radio_test.dart` (only if a listing method is added)
|
||
|
||
- [x] 15.1 RED — storage progress-bar fill reflects used/total (e.g. 84/200 MB fixture) with a caption stating both
|
||
values.
|
||
- [x] 15.2 RED — 3 recording-file fixtures render as 3 rows (name/date/duration/size); an empty folder renders an
|
||
empty state, not an error; tapping play starts/stops playback.
|
||
- [x] 15.3 RED — the "⋮" menu exposes exactly Rename/Share/Delete — constrained to what
|
||
`servicio_grabacion_radio.dart` already exposes, no extra action.
|
||
- [x] 15.4 RED — Delete (file + row removed), Rename (persists across reload), Share (invokes platform share sheet)
|
||
scenarios. **Delete and Rename widget-level scenarios are written but `skip: true`** (Share is not — see
|
||
below): both hang indefinitely the instant `EstadoGrabacion` wires to a bare, real `ServicioGrabacionRadio`
|
||
pointed at a real directory inside a `testWidgets()` body — a combination no other test in this codebase
|
||
uses. Four independent causes were ruled out at apply time (a directory-path off-by-one;
|
||
`Directory.list()`'s async stream vs `listSync()`; the confirm-dialog interaction specifically — a
|
||
render-only reproduction with zero taps hangs identically; `WidgetTester.runAsync()`, Flutter's own
|
||
documented escape hatch for real I/O during widget lifecycle). The underlying logic is NOT unproven:
|
||
`eliminarGrabacion` and `renombrarGrabacion` both have passing unit tests in
|
||
`servicio_grabacion_radio_test.dart` (plain `test()`, exercised against real files, no hang) — the one
|
||
variable no diagnostic could change was `testWidgets()` itself. Documented inline in both skipped tests;
|
||
flagged for a fresh Windows-process debugging pass in a future batch, not treated as a code defect.
|
||
- [x] 15.5 GREEN — added `listarGrabaciones()` to `servicio_grabacion_radio.dart` (pure filesystem read via
|
||
`listSync`/`statSync`, sorted most-recent-first, empty list for a missing directory — never throws). Also
|
||
added `eliminarGrabacion(ruta)` and `renombrarGrabacion(ruta, nuevoNombre)` — file-lifecycle management for
|
||
files this service already creates, not new capabilities; both needed for the constrained "⋮" menu (task
|
||
15.3/15.4), both via plain `dart:io`, no new conceptual surface. `EstadoGrabacion` gained thin delegates for
|
||
all three plus `notifyListeners()`. New `lib/modelos/archivo_grabacion.dart` (ruta, nombre, fecha,
|
||
tamanoBytes — pure filesystem metadata, no audio decoding).
|
||
- [x] 15.6 GREEN — built `lib/pantallas/pantalla_grabaciones.dart`: storage bar (`LinearProgressIndicator` over
|
||
`EstadoGrabacion.maxBytes` vs. the summed listing), per-recording rows (name/date/duration/size) with inline
|
||
play/pause, "⋮" menu (`PopupMenuButton`, exactly 3 items). New `ReproductorGrabaciones` abstraction
|
||
(duration-lookup + toggle-play) keeps `just_audio.AudioPlayer` out of `ServicioAudio` (never touched — hard
|
||
constraint) and out of `servicio_grabacion_radio.dart`; the real `just_audio`-backed implementation is
|
||
static-review-only (same documented constraint as `PluriWaveAudioHandler` in `cola_local_test.dart` — a real
|
||
`AudioPlayer` needs platform `MethodChannel`s this suite does not mock), every test injects a fake. `Share`
|
||
is similarly injected (`compartir` constructor parameter), defaulting to the real `share_plus` call. 12 new
|
||
ARB keys (en/es only, matching WU1/3a/3b precedent): `recordingsLibraryTitle`,
|
||
`recordingsLibraryStorageCaption`, `recordingsLibraryEmptyTitle`, `recordingsLibraryEmptySubtitle`,
|
||
`recordingActionRename/Share/Delete`, `recordingRenameDialogTitle/Label/EmptyError`,
|
||
`recordingDeleteConfirmTitle/Message` — a brand-new screen needs new copy, unlike WU3a/WU3b's move-only
|
||
screens.
|
||
- [x] 15.7 REFACTOR — confirmed the menu cannot expose a 4th action (task 15.3's test asserts `PopupMenuItem`
|
||
count is exactly 3); confirmed the empty-folder scenario renders `PluriEmptyState`, not a crash or exception
|
||
(`tester.takeException()` asserted null).
|
||
- [x] 15.8 Verify — scoped verify green: 17 passed, 2 skipped (documented above), 0 failed — the menu-action
|
||
scenarios remain exercised end-to-end for Share (15.4-C) plus both file-mutation methods proven at the
|
||
service level. Empty-folder scenario green. Full suite: 604/604 green (2 skipped), up from 592. `flutter
|
||
analyze`: 1 issue, identical to baseline.
|
||
|
||
**Known plan gap, not fixed here (out of WU15's task list, flagged for the orchestrator):** no task in WU15 — or
|
||
anywhere else in this file — wires a navigation entry point to `PantallaGrabaciones`. It is a genuinely new,
|
||
currently-unreachable route. `PantallaAjustesGrabaciones` (WU3b) is a different screen (folder/size-limit settings)
|
||
and no task asks it to gain a "view library" link. WU15's own **Verify** command lists only
|
||
`pantalla_grabaciones_test.dart` and `servicio_grabacion_radio_test.dart` — not
|
||
`pantalla_ajustes_grabaciones_test.dart` — confirming this screen is not meant to touch WU3b's file in this work
|
||
unit. Not invented here; needs a design decision, not a guess.
|
||
|
||
## WU15b — Wire the recordings library into Settings navigation
|
||
|
||
**Not in the original plan — added to close the WU15 gap noted above.** WU15 built `PantallaGrabaciones` (the
|
||
recordings library) fully tested and committed, but left it unreachable from the app: `rg "PantallaGrabaciones" lib/`
|
||
found only its own declaration. This work unit exists solely to fix that.
|
||
|
||
**Commit**: `fix(grabaciones): wire the recordings library into Settings navigation`
|
||
**Depends on**: WU15
|
||
**Spec refs**: `recordings-library` — Browsable Recordings List (reachability); `app-navigation-shell` — Push-Chrome
|
||
on Second-Level Screens (the settings form stays a pushed screen, now one level deeper)
|
||
**Verify**: `flutter test test/pantallas/pantalla_ajustes_test.dart test/pantallas/pantalla_grabaciones_test.dart test/pantallas/ajustes/pantalla_ajustes_grabaciones_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --cached --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: `pantalla_ajustes_test.dart` (one scenario re-targeted), `pantalla_grabaciones_test.dart` (one new
|
||
scenario)
|
||
|
||
**Coordinator ruling applied.** The approved mockup's screen 12 ("Ajustes › Grabaciones") depicts the recordings
|
||
**library**, not the settings form — so the `GRABACIONES Y MÚSICA` group's "Grabaciones" row now opens
|
||
`PantallaGrabaciones` (the library) instead of `PantallaAjustesGrabaciones` (the folder/size-limit settings form).
|
||
The settings form is not dropped: it stays reachable, but now from **within** the library, via a settings icon in its
|
||
`PluriPushScaffold.actions` (matching the existing `pantalla_ajustes_timer_sueno.dart` "Add" action precedent for a
|
||
real capability living in the header, not decorative chrome).
|
||
|
||
- [x] 15b.1 RED — re-targeted `pantalla_ajustes_test.dart`'s "tapping the Grabaciones row" scenario: it now asserts
|
||
landing on `PantallaGrabaciones` (`find.text('My recordings')`), not the settings form (`find.text('Change
|
||
path')` asserted absent).
|
||
- [x] 15b.2 RED — added a new scenario to `pantalla_grabaciones_test.dart`: tapping the library's settings icon
|
||
pushes a second `PluriPushScaffold` showing the settings form (`find.text('Change path')`).
|
||
- [x] 15b.3 GREEN — `pantalla_ajustes.dart`: the "Grabaciones" `FilaAjuste.onTap` now pushes `PantallaGrabaciones`;
|
||
removed the now-unused `PantallaAjustesGrabaciones` import from this file.
|
||
- [x] 15b.4 GREEN — `pantalla_grabaciones.dart`: added `actions: [IconButton(Icons.settings_outlined, ...)]` to its
|
||
`PluriPushScaffold`, pushing `PantallaAjustesGrabaciones`. One new ARB key (en/es only, matching precedent):
|
||
`recordingsLibrarySettingsTooltip`.
|
||
- [x] 15b.5 REFACTOR — confirmed the new test needed the same `_suppressListTileInkAssertion()` helper WU3a/WU3b
|
||
established (WU3b Discovery 9: any pushed screen whose `PluriGlassSurface` body contains a tappable `ListTile`
|
||
needs it) — added to `pantalla_grabaciones_test.dart`, scoped to only the one new scenario that mounts
|
||
`PantallaAjustesGrabaciones`.
|
||
- [x] 15b.6 Verify — scoped suite green: 605/605 (up from 604), 2 skipped (unchanged, both pre-existing WU15
|
||
documented skips). `flutter analyze`: 1 issue, identical to baseline. Literal-encoding scan: zero hits.
|
||
|
||
|
||
|
||
**Commit**: `feat(connectivity): restyle offline and reconnect banners`
|
||
**Depends on**: WU1
|
||
**Spec refs**: crosscutting risk register (reconnect attempt counter is verify-first, not assumed)
|
||
**Verify**: `flutter test test/widgets/reconnect_ui_test.dart test/servicios/servicio_audio_reconnect_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: `reconnect_ui_test.dart`. `servicio_audio_reconnect_test.dart` must pass unmodified.
|
||
|
||
- [ ] 16.1 Verify-first — run `servicio_audio_reconnect_test.dart` and inspect the reconnect controller: confirm
|
||
whether it tracks an attempt count.
|
||
- [ ] 16.2 RED — restyle test for the offline banner (visual only) and, **only if 16.1 confirms an attempt count
|
||
exists**, a test for the "Reconectando · intento N de M" label.
|
||
- [ ] 16.3 GREEN — restyle the offline banner to the new visual language.
|
||
- [ ] 16.4 GREEN — **conditionally**: wire the attempt-count label only if the controller exposes one; otherwise
|
||
ship the restyle without the counter — do not add new plumbing to invent one.
|
||
- [ ] 16.5 REFACTOR — confirm shimmer skeletons (`TarjetaEmisoraShimmer`) are unchanged.
|
||
- [ ] 16.6 Verify — `reconnect_ui_test.dart` updated and green; `servicio_audio_reconnect_test.dart` unmodified and
|
||
green.
|
||
|
||
## WU17 — Welcome / onboarding screen
|
||
|
||
**Commit**: `feat(bienvenida): add monetization-free welcome screen`
|
||
**Depends on**: WU1
|
||
**Spec refs**: `onboarding-welcome` — Full-Screen Welcome Route, No Monetization Content, CTA Dismisses to Escuchar
|
||
**Verify**: `flutter test test/pantallas/pantalla_bienvenida_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `pantalla_bienvenida_test.dart`
|
||
|
||
- [ ] 17.1 RED — the welcome screen renders as a full-screen route (not a modal `Dialog`) with a logo, headline,
|
||
body copy, exactly 3 feature bullets, and exactly 1 primary CTA.
|
||
- [ ] 17.2 RED — binding no-monetization guard: scan the rendered widget tree's text content and assert it contains
|
||
none of: the substring "PRO", a currency amount, or a day-count trial phrase; no secondary "free version"
|
||
link.
|
||
- [ ] 17.3 RED — tapping "Empezar a escuchar" lands on the Escuchar tab (index 0) with the welcome route removed
|
||
from the back stack.
|
||
- [ ] 17.4 GREEN — build `lib/pantallas/pantalla_bienvenida.dart` as a full-screen route; content only (logo /
|
||
headline / body / 3 bullets / 1 CTA) — no PRO pill, no trial duration, no price, no secondary link.
|
||
- [ ] 17.5 GREEN — wire the CTA to dismiss-and-navigate to Escuchar with the route removed.
|
||
- [ ] 17.6 REFACTOR — grep `lib/l10n/app_*.arb` for "PRO"/price/day-count trial strings introduced by this WU;
|
||
confirm none exist.
|
||
- [ ] 17.7 Verify — all 3 scenario tests green; grep scan clean.
|
||
|
||
## WU18 — i18n batch (all 13 locales)
|
||
|
||
**Commit**: `feat(i18n): add redesign strings and translate Escuchar rename to 11 locales`
|
||
**Depends on**: WU1-WU17 (runs last, after `en`/`es` land across all slices)
|
||
**Spec refs**: `app-navigation-shell` — Escuchar Tab Rename Preserves the ARB Key (11-locale completion); backs the
|
||
UI copy every other capability introduced
|
||
**Verify**: `flutter test && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**Modified tests**: ARB key-parity guard test (extend existing or add one if none exists)
|
||
|
||
- [ ] 18.1 RED — an ARB-parity test asserting all 13 `lib/l10n/app_*.arb` files contain an identical key set.
|
||
- [ ] 18.1b RED — **anti-copy guard (a key-parity test is NOT enough).** `l10n.yaml` sets
|
||
`template-arb-file: app_es.arb`, so any key missing from a locale silently falls back to **Spanish** — that is
|
||
why untranslated strings currently render as Spanish inside `app_localizations_ja.dart`, `_fr.dart`, etc.
|
||
A key-parity test would pass just as happily if all 11 locales simply COPIED the Spanish value.
|
||
Write a test that, for every non-`es` locale, collects every key whose value is byte-identical to the `app_es.arb`
|
||
value and requires each one to appear in an explicit allowlist (a checked-in file, e.g.
|
||
`test/l10n/identical_value_allowlist.dart`). Legitimately-identical values exist — brand names, "OK", "Radio",
|
||
codec names, pure symbols — so the allowlist is the escape hatch, but **each entry must be added deliberately,
|
||
never in bulk**. Any unlisted identical value fails the test.
|
||
Seed the allowlist ONLY with entries that are genuinely untranslatable; do not pre-populate it to make the
|
||
suite green.
|
||
- [ ] 18.2 RED — `navHome` exists as a key in all 13 locales, and `git diff` for this commit touches exactly 11
|
||
`navHome` **value** lines (en/es were WU1's job, task 1.9).
|
||
- [ ] 18.3 GREEN — add the ~35-45 new keys (accumulated from WU4-WU17's English/Spanish-only strings) to all 13
|
||
ARB files, sourcing translations for the 11 non-en/es locales.
|
||
- [ ] 18.4 GREEN — set the `navHome` value in the remaining 11 locales to a real translation of "Listen" (not a
|
||
mechanical copy of the English/Spanish pair).
|
||
- [ ] 18.5 REFACTOR — run the ARB-parity check across all 13 files; confirm zero missing/extra keys, AND that the
|
||
18.1b anti-copy guard passes with an allowlist containing only genuinely-untranslatable entries.
|
||
Sanity figures at the time this was written: `app_es.arb` (template) 540 keys, `app_en.arb` 561, and all 11
|
||
other locales 518 — i.e. the non-template locales were **22 keys behind**, and those 22 were rendering in
|
||
Spanish. WU18 is what closes that gap; the count will have grown as WU8-WU17 land, so re-measure rather than
|
||
trusting these numbers.
|
||
- [ ] 18.6 Verify — full `flutter test` suite green; `flutter analyze` clean; scoped `dart format` over the files
|
||
this change touched produces no diff (NEVER bare `dart format .` — see the warning at the top);
|
||
grep confirms no "PRO"/price/trial-duration string exists in any ARB file (final whole-app Success Criteria
|
||
check).
|
||
|
||
---
|
||
|
||
## Final Verification Sequence (run once, after WU18's commit)
|
||
|
||
1. `flutter test` — full suite green.
|
||
2. `flutter analyze` — clean.
|
||
3. `dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM main -- '*.dart')` — no diff over the
|
||
files this change touched. **NEVER bare `dart format .`** — it reformats 27 unrelated pre-existing files and would
|
||
break the empty-diff guards in steps 4 and 5 below (Engram `reference/dart-format-scope-hazard`, id 2511).
|
||
4. `git diff --stat lib/servicios/navegacion_auto.dart lib/servicios/servicio_ecualizador.dart` — both empty.
|
||
5. `git diff lib/servicios/servicio_audio.dart` — band-application block (`749-762`) unchanged.
|
||
6. Grep `lib/` for `countrycodes` — zero hits.
|
||
7. Grep `lib/l10n/app_*.arb` for "PRO", a currency amount, or a day-count trial phrase — zero hits.
|
||
8. Confirm all 13 ARB files have identical key sets and `navHome` present in each.
|
||
9. Confirm the equalizer screen renders exactly 5 sliders.
|
||
10. Run `test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart` in isolation — passes, file diff empty.
|
||
|
||
## Next Step
|
||
|
||
`sdd-apply` — implement WU1 first (unblocks everything else); honor the `size:exception` commits (WU3a, WU3b, WU10)
|
||
as recorded above rather than attempting to split them further.
|