1180 lines
107 KiB
Markdown
1180 lines
107 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.
|
||
> **WU17b was added mid-apply, not planned upfront — same shape as WU15b.** WU17 shipped `PantallaBienvenida` (the
|
||
> welcome screen) fully tested but reachable from nowhere in the app (`rg "PantallaBienvenida" lib/app.dart lib/main.dart`
|
||
> found nothing). WU17b (below, after WU17's section) wires it into the genuine first-launch flow and establishes how
|
||
> it coexists with the pre-existing, unrelated `PluriOnboardingDialog` ("what's new" modal). 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~~ → **REALIZED: 1,871** (1,793+ / 78-, 25 files) | Medium-High | **Yes — retroactive, see WU13 section** |
|
||
| 14 | `feat(reproductor): restructure full player with tool-tray and EQ sheet` | 13 | ~~450-600~~ → **REALIZED: 1,335** (1,114+ / 221-, 19 files) | Medium-High | **Yes — retroactive, see WU14 section** |
|
||
| 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~~ → **REALIZED: 361** (screen + test, 313 / ARB 22 / docs 48) — see the WU15-precedent note below | Low | No — same reasoning as WU15§ |
|
||
| 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).
|
||
**WU17 hit the identical pattern**: 150-200 covered only the screen itself; realized 361 lines across the screen
|
||
(169) + its test (144) + 9 new ARB keys × 2 locales (22) + this docs update (48), PLUS 13 regenerated
|
||
`lib/l10n/gen/*.dart` files (~430 more lines, untouched by hand) for a raw `git diff --stat` total of 814. Same
|
||
ruling: not `size:exception` — one new, cleanly-scoped screen with no split candidate, all-additions except the ARB
|
||
files (which gain lines, not churn).
|
||
|
||
```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
|
||
|
||
**`size:exception` — realized diff exceeded the ~500-line threshold.** Commit `9a2eb57`: **1,113 changed lines**
|
||
(876+/237-) across 19 files. Breakdown: `pantalla_alarmas.dart` 485, ARB sources 54 (27 en + 27 es), 13 regenerated
|
||
`lib/l10n/gen/*.dart` files ~300, new/modified test file 189, this docs file 37 — the same "a strict-TDD commit
|
||
carries its test files, and any ARB touch drags 13 generated files with it" pattern as WU3a/WU7/WU15 (Engram
|
||
`reference/estimating-strict-tdd-diffs`, id 2514). Justification per the forecast table's own footnote: "largest
|
||
single alarm-card + hero + vacation-summary restyle, not divisible without breaking the one-commit-per-work-unit
|
||
rule" — the card simplification, hero skip pill, and vacation summary row are one cohesive visual/behavioral change
|
||
to one screen file; splitting them into separate commits would leave an inconsistent intermediate UI (e.g. a hero
|
||
with a skip pill but cards still showing the old button row).
|
||
|
||
- [x] 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.
|
||
- [x] 8.2 RED — the hero banner's inline "Saltar" action skips the featured (soonest-firing) alarm, consistent with
|
||
existing skip-next behaviour.
|
||
- [x] 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).
|
||
- [x] 8.4 GREEN — simplify `_TarjetaAlarma` to giant time/name/switch; move Edit/Skip/Delete behind tap/swipe.
|
||
**Corrected at apply time**: proposal.md's own WU8 row says "giant time + station + switch" (not "name") —
|
||
the card shows the STATION (or `noStationUseInternalSound`), not the alarm's custom name; the name stays
|
||
editable inside the editor sheet, unchanged. Tap-to-edit and swipe-to-delete-with-confirmation implemented via
|
||
`Dismissible` (`confirmDismiss` shows an `AlertDialog`, `onDismissed` calls the existing `eliminarAlarma`) plus
|
||
an inner `Material(type: MaterialType.transparency) > InkWell` (the known `PluriGlassSurface`-ink-splash hazard
|
||
from WU7, pre-empted here rather than rediscovered). All 4 old per-card chips (repeat/vacation/volume/fade) and
|
||
notice lines (next-execution/skipped/vacation-paused) are dropped from the collapsed card per "minimal cards"
|
||
— still reachable via the editor once opened; only the hard-guarded trio (edit/skip/delete) is a capability
|
||
that must never be lost, and it isn't.
|
||
- [x] 8.5 GREEN — add the hero banner's inline "Saltar" pill calling the existing skip-next path.
|
||
- [x] 8.6 GREEN — replace `_PanelVacaciones`'s inline list with a summary row wired to push the Vacaciones manager
|
||
screen. **Design decision, not specified by any ADR (WU8 has none)**: the summary row's "range count +
|
||
next-range countdown" is computed directly over the existing `estado.vacaciones` (no new `EstadoAlarmas` query
|
||
method — ADR-6's pure query additions are WU9's job). The push target is `_PantallaVacacionesTemporal`, a
|
||
private placeholder holding the OLD inline panel's body verbatim (hint text, "Add" action, per-range delete) —
|
||
same "keep the real action, drop only the header" rule WU3a/WU3b established — so add/delete-range capability
|
||
is never dropped, not even for one commit, and WU9 replaces this whole widget with the real `PantallaVacaciones`.
|
||
- [x] 8.7 REFACTOR — confirm no scheduling/dismiss-guard code path was touched; `estado_alarmas_test.dart` and
|
||
`estado_alarmas_snooze_test.dart` pass unmodified. Also removed now-dead code the simplification orphaned:
|
||
the `_InfoChip` widget and the `_programacion` helper (both were only reachable from the old chips row).
|
||
- [x] 8.8 Verify — tap/swipe/hero-skip scenarios all green; confirmed
|
||
`pantalla_alarma_sonando_dismiss_guard_test.dart` was not touched by this WU (it only edits the root list
|
||
screen) — re-ran it alongside the scoped suite, byte-identical, all 8 cases green.
|
||
|
||
## 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')`
|
||
|
||
**`size:exception` — realized diff exceeded the ~350-450 forecast.** Commit `9dfcf0b`: **1,822 changed lines**
|
||
(1,432+/390-) across 23 files. Breakdown: `estado_alarmas_test.dart` 558 (**mostly `dart format` reformatting
|
||
pre-existing, unrelated test blocks it happened to touch** — the same Dart-SDK-3.12.0 skew as Engram
|
||
`reference/dart-format-scope-hazard` id 2511, here as an unavoidable side effect of the mandatory scoped-format
|
||
gate on a file this WU legitimately edits, not new test logic; the actual new WU9 tests are ~215 of those lines),
|
||
new `pantalla_vacaciones.dart` 353, 14 regenerated `lib/l10n/gen/*.dart` files 334, new `pantalla_vacaciones_test.dart`
|
||
224, `pantalla_alarmas.dart` 197 (net code MOVED OUT to the new screen file, not new logic), `estado_alarmas.dart`
|
||
63, `alarma_musical.dart` 14, ARB sources 32, this docs file 47. Same "test files + generated l10n + reformatting
|
||
of touched files aren't counted by a lib/-only estimate" pattern as every prior WU (Engram
|
||
`reference/estimating-strict-tdd-diffs`, id 2514). Justification: the 4 query methods, the model addition, and the
|
||
new screen are one cohesive vertical slice (state + model + UI) that WU8 explicitly deferred to this commit;
|
||
splitting further would leave either dead query methods with no UI consumer or a UI screen with no queries to call.
|
||
**New tests**: `pantalla_vacaciones_test.dart`
|
||
**Modified tests**: `estado_alarmas_test.dart` (additions only)
|
||
|
||
- [x] 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. **Correction found at apply time**: `DateTime(...)`
|
||
is NOT a const constructor in Dart (only literal-free `const` values are) — the first draft wrote `const
|
||
RangoVacaciones(..., inicio: DateTime(...))` and failed to compile; fixed by dropping `const` on every
|
||
`RangoVacaciones` fixture built from a `DateTime(...)` call, same non-const pattern the pre-existing
|
||
`AlarmaMusical` fixtures in this file already use whenever they set a `DateTime` field.
|
||
- [x] 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. Added a 4th case (inactive alarm counts toward
|
||
neither list) and a 5th purity guard (none of the 4 query methods call `android.programar`).
|
||
- [x] 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. **Correction found
|
||
at apply time**: `PantallaVacaciones` calls the new `EstadoAlarmas` query methods with NO `ahora` argument
|
||
(production code always uses real `DateTime.now()` — the `{DateTime? ahora}` clock injection exists for STATE-
|
||
layer tests only, per ADR-6's own explicit rejection of a testable Clock abstraction). The first draft used
|
||
hardcoded calendar-literal vacation ranges assuming a fixed "today", which silently exercised the WRONG
|
||
code path once real time didn't match; fixed by building every fixture's `RangoVacaciones` dates relative to
|
||
the real `DateTime.now()` (e.g. `hoyDia.add(Duration(days: 5))`) instead.
|
||
- [x] 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)`.
|
||
- [x] 9.5 GREEN — add `ImpactoVacaciones` beside `RangoVacaciones` in `lib/modelos/alarma_musical.dart`.
|
||
- [x] 9.6 GREEN — build `lib/pantallas/pantalla_vacaciones.dart` as a `PluriPushScaffold`; wire WU8's summary-row
|
||
tap to it. **Apply-time simplification, not spec-tested**: the "Añadir rango" CTA is a solid `OutlinedButton`,
|
||
not a literally dashed border — no dashed-border utility exists in this codebase beyond WU4's own private
|
||
one-off `_DashedBorderPainter` (`pantalla_favoritos.dart`), and duplicating a cosmetic `CustomPainter` for a
|
||
detail no GIVEN/WHEN/THEN scenario actually tests isn't justified. The add-range form (`_EditorVacacionesSheet`,
|
||
`_PickerButton`) was MOVED verbatim from `pantalla_alarmas.dart` (its only remaining consumer after WU8's
|
||
placeholder is deleted below) rather than duplicated.
|
||
- [x] 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. Also deleted WU8's own
|
||
placeholder (`_PantallaVacacionesTemporal`) and the now-dead `_nombreVisibleVacaciones` wrapper from
|
||
`pantalla_alarmas.dart`, and rewired the summary row's `onTap` to `PantallaVacaciones`.
|
||
- [x] 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). Confirmed via `git diff`
|
||
(empty) and a full re-run (all 8 cases green) alongside the WU9 scoped suite.
|
||
|
||
## 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`
|
||
|
||
- [x] 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. **Correction found at apply time**: Flutter
|
||
requires a `Semantics` node exposing `onIncrease`/`onDecrease` to ALSO carry `increasedValue`/`decreasedValue`
|
||
whenever `value` is set (`SemanticsNode.updateWith`'s `(value == '') == (increasedValue == '')` assertion) —
|
||
added both, computed from the same pure delta-preview helpers the mutation uses. Also had to set
|
||
`excludeSemantics: true` on each segment's `Semantics` node: without it, the inner `GestureDetector`'s own
|
||
semantics contribution merged upward and (a) duplicated the digits into `label` via the child `Text`'s
|
||
implicit semantics and (b) auto-exposed `scrollUp`/`scrollDown` (Flutter's default accessibility mapping for
|
||
a registered vertical-drag recognizer) — neither wanted. Every action (`tap`/`increase`/`decrease`) is now
|
||
declared explicitly on the one Semantics node instead.
|
||
- [x] 10.2 GREEN — implement `lib/widgets/editor_hora_inline.dart` (giant inline HH:MM display, drag/tap-to-adjust).
|
||
Tap increments by one step (wrapping); vertical drag adjusts continuously (24px/step, chosen for a
|
||
comfortable scrub distance — not a reused measured constant, this widget has no other consumer).
|
||
- [x] 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.
|
||
**Correction found at apply time**: this file previously held ONLY the 2 pure `fechaCortaLocalizada` format
|
||
tests, no widget coverage at all — extended it in place (kept both untouched) rather than creating a second
|
||
file, since the task's own instruction was to "update" this file. Also required a one-line surgical fix to
|
||
the PRE-EXISTING fallback-station test in `pantalla_alarmas_editor_test.dart` (not itself listed as a
|
||
"Modified test" for this WU): a collapsed `ExpansionTile` does not build its children, so
|
||
`find.byKey('alarm-fallback-station-field')` found nothing until the "Advanced" header is tapped first —
|
||
added that one tap, no scenario/assertion changed.
|
||
- [x] 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. **Design decision, not specified by any ADR (WU10 has none)**: the
|
||
one-time/daily/weekly `SegmentedButton` stays in the main flow (not Advanced) since it gates which weekday
|
||
circles are enabled; weekday circles now render unconditionally with `onSelected: null` (Material's standard
|
||
disabled-chip state) when `_tipo != diasSemana`, instead of being removed from the tree — no change to
|
||
`_tipo`/`diasSemana`/`fechaUnica` persistence logic, confirmed the scheduler treats `diaria` and
|
||
`diasSemana` as genuinely different code paths (`_buscarDiaria` vs `_buscarPorDiasSemana` in
|
||
`servicio_programacion_alarmas.dart`), so the two were never merged.
|
||
- [x] 10.5 GREEN — restyle the volume/fade-in sliders (cosmetic only, no behaviour change): swapped the raw
|
||
`Theme.of(context).textTheme` lookups on `_SectionLabel` ("Sonido y volumen") and the fade-in `ListTile`
|
||
title for `context.pluriType.cardTitle` — same weight class, now sharing the named type-scale token with
|
||
every other card/section title in the redesign. `Slider` widget types and their `min`/`max`/`value` wiring
|
||
are byte-identical, so `'el slider de volumen permite bajar hasta 0.0 (S2-R11)'` passes unmodified.
|
||
- [x] 10.6 REFACTOR — confirmed the dismiss-guard test is untouched by this WU (editor sheet only, not the ringing
|
||
screen): `git diff` empty for both `pantalla_alarma_sonando.dart` and
|
||
`pantalla_alarma_sonando_dismiss_guard_test.dart`; also removed the now-dead `_elegirHora()` method (its only
|
||
caller, the old time-picker `_PickerButton`, no longer exists).
|
||
- [x] 10.7 Verify — standalone widget test green (7/7); date/fallback/sound round-trip tests green (4/4 new in
|
||
`pantalla_alarmas_fecha_test.dart`); full WU10-scoped run (`editor_hora_inline_test.dart` +
|
||
`pantalla_alarmas_fecha_test.dart` + `pantalla_alarmas_editor_test.dart` + the two `estado_alarmas*` guard
|
||
files + all 3 `pantalla_alarma_sonando*` files + `pantalla_vacaciones_test.dart`): 82/82 green. `flutter
|
||
analyze`: 1 issue, identical to baseline.
|
||
|
||
**`size:exception` recorded.** Realized: **993 changed lines** (891+/102-) across 21 files — over the 500-650
|
||
forecast, same "a strict-TDD commit carries its test files, and any ARB touch drags 13 generated files with it"
|
||
pattern as every prior WU (Engram `reference/estimating-strict-tdd-diffs`, id 2514). Breakdown: `lib/` production
|
||
code alone is 429 lines (`pantalla_alarmas.dart` 123+/102-, new `editor_hora_inline.dart` 204+/0-) — within the
|
||
forecast band on its own; the 3 new/modified test files add 376 lines and the 4 new ARB keys drag in 13 regenerated
|
||
`lib/l10n/gen/*.dart` files plus the 2 ARB sources for 188 more. Justification unchanged from the forecast table's
|
||
own footnote ‡: "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)."
|
||
|
||
## 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.
|
||
|
||
- [x] 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. **Correction found at apply time**: the pre-existing
|
||
"3/5/10 mas el personalizado" test (snoozeMinutos=7 growing a 4th button) is exactly the behavior this WU
|
||
replaces — rewrote it in place (not deleted) to assert the new "always exactly 3, no 4th tile" contract,
|
||
plus 2 new tests pinning which tile is `FilledButton` (highlighted) vs `OutlinedButton`.
|
||
- [x] 11.2 RED — the status label renders "Subiendo volumen" (or localized equivalent) with no accompanying
|
||
numeric value that changes over time. New ARB key `alarmVolumeRisingStatus` carries NO placeholder at all
|
||
(unlike `alarmFadeInSummary`, which has `{seconds}`) — by construction this label can never grow a live
|
||
counter without a deliberate key change. Gated on `alarma.fadeInSegundos > 0`: the existing `_montarPantalla`
|
||
test helper in `pantalla_alarma_sonando_test.dart` already carried an unused `fadeInSegundos` parameter
|
||
defaulting to 0, confirming this gate was anticipated ahead of this WU.
|
||
- [x] 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. No per-station artwork exists in this codebase (`Emisora.favicon` is a
|
||
network URL — rendering it via `Image.network` in a widget test hangs/throws without a mocked
|
||
`HttpClient`), so the existing bundled `alarm_music.png` asset is reused, blurred (`ImageFiltered`,
|
||
sigma 44) and stretched — decorative, not spec-tested.
|
||
- [x] 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. `_bannerFalloDetencion` is
|
||
BYTE-IDENTICAL to its pre-WU11 form (same l10n keys, same widget structure) — only repositioned, never
|
||
rewritten, so every existing force-stop test (SS-3a/SS-3b/SS-3c/RES-2) keeps passing unmodified. **Design
|
||
decision, not specified by any ADR (WU11 has none)**: the "Subiendo volumen" status dot is intentionally
|
||
NOT animated/pulsing — an `AnimationController.repeat()` here would reintroduce the exact
|
||
"`pumpAndSettle()` never terminates" hazard WU5 documented for `VisualizadorAudio`.
|
||
- [x] 11.5 REFACTOR — ran `pantalla_alarma_sonando_dismiss_guard_test.dart` **unchanged**: all 8 cases green,
|
||
byte-identical, no edit needed or made.
|
||
- [x] 11.6 Verify — force-stop banner still appears on a simulated stop failure (all 3 force-stop scenarios green,
|
||
unmodified); dismiss-guard test file diff is empty against BOTH `HEAD` and `main`.
|
||
|
||
**`size:exception` recorded.** Realized: **530 changed lines** (440+/90-) across 3 files against the 200-300
|
||
forecast — `lib/` production code alone is 302 lines (248+/54-), essentially at the top edge of the forecast band
|
||
by itself; the 2 modified test files add 228 lines, the same "a strict-TDD commit carries its test files" pattern
|
||
as every prior WU (Engram `reference/estimating-strict-tdd-diffs`, id 2514) — this WU touches no ARB keys beyond
|
||
the 1 new `alarmVolumeRisingStatus` string, so no 13-file l10n/gen inflation this time; test growth alone accounts
|
||
for the overage. Not pre-flagged for exception in the forecast table (unlike WU10), but the same "one cohesive
|
||
visual+behavioral restyle to one safety-critical screen" reasoning applies: splitting the background/snooze-tiles/
|
||
stop-pill/status-label changes into separate commits would leave an inconsistent intermediate UI on the ringing
|
||
screen specifically — the one screen in this whole branch where an inconsistent intermediate state is least
|
||
acceptable.
|
||
|
||
## 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.
|
||
|
||
- [x] 13.1 RED — the Ecualizador screen renders exactly **5** vertical sliders (regression guard — reject any
|
||
future 7-slider change on sight). **Corrected at apply time**: named test file `ecualizador_widget_test.dart`
|
||
did not exist before this commit (`ecualizador_widget.dart` had zero coverage) — created new, plus the
|
||
screen-level assertion in `pantalla_ajustes_ecualizador_test.dart` (the spec scenario's own GIVEN is "the
|
||
Ecualizador settings screen").
|
||
- [x] 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`.
|
||
- [x] 13.3 RED — submitting an empty/whitespace-only preset name shows a validation message and persists nothing.
|
||
Covered at both the state layer (new `estado_ecualizador_presets_personalizados_test.dart` — see 13.7's note)
|
||
and the UI layer (`pantalla_ajustes_ecualizador_test.dart`'s "Guardar como preset" dialog scenarios).
|
||
- [x] 13.4 RED — the "Emisoras con ajuste propio" drill-down lists exactly the stations present in
|
||
`presetsPorEmisora`.
|
||
- [x] 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.
|
||
- [x] 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.
|
||
- [x] 13.7 GREEN — extend `EstadoEcualizador` with `presetsPersonalizados`, `guardarPresetPersonalizado(nombre)`,
|
||
`eliminarPresetPersonalizado(nombre)`, reading/writing only through the new service. **Correction found at
|
||
apply time**: the new custom-preset LOAD (`cargarPresetsPersonalizados()`) is deliberately NOT folded into
|
||
`cargarPersistido()` — that method is exercised ~30 times by `estado_ecualizador_test.dart` (one of the three
|
||
protected EQ test files) via Fakes for `servicio`/`dispositivoAudio` ONLY, with zero SharedPreferences
|
||
awareness anywhere in that file; adding a third always-real-by-default collaborator to it would have
|
||
introduced real SharedPreferences I/O into every one of those cases. Called explicitly from the screen's
|
||
`initState` instead (same pattern `refrescarDispositivoActual` already uses). New coverage lives in a NEW
|
||
file, `test/estado/estado_ecualizador_presets_personalizados_test.dart` — NOT added to
|
||
`estado_ecualizador_test.dart`, which stays unmodified.
|
||
- [x] 13.8 GREEN — restyle `lib/widgets/ecualizador_widget.dart` in place: strip the internal title + preset chip
|
||
row (re-verified at the CURRENT lines 59-77 of the pre-WU13 file, not trusted from the design doc's
|
||
reference), restyle the 5 `Card`/`RotatedBox`/`Slider` tiles to the vertical-track look (token-driven accent
|
||
colour, `AnimatedOpacity` grey-out), add the `habilitado` parameter (disables + greys every slider).
|
||
`PresetsEcualizadorWidget` additively widened with an optional `personalizados` list (default empty) so
|
||
custom presets can join the chip row without a second implementation. Kept the `for (int i = 0; i < 5; i++)`
|
||
loop bound literally `5`.
|
||
- [x] 13.9 GREEN — build out `lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart` (the WU3a placeholder body)
|
||
wiring `EstadoEcualizador.presetPrincipal` / `cambiarPresetPrincipal`, surfacing "Salida activa" (own private
|
||
row, live via the existing device-change `notifyListeners()`) and the drill-down row (own private row +
|
||
destination screen, station uuids resolved against `EstadoRadio.listaFavoritos` with a raw-uuid fallback for
|
||
a non-favorite station). Added the base-vs-per-station explainer banner and the "Guardar como preset" action
|
||
+ dialog (validates via `guardarPresetPersonalizado`, shows an inline `errorText` on empty/whitespace name).
|
||
- [x] 13.10 REFACTOR — reviewer check: confirmed `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` (this WU never touches `servicio_audio.dart` at all).
|
||
- [x] 13.11 Verify — the three EQ test files (`servicio_ecualizador_test.dart`, `estado_ecualizador_test.dart`,
|
||
`servicio_audio_eq_reapply_test.dart`) pass **unmodified**; exactly 5 sliders render (component AND screen
|
||
level). Full suite: 713/713 green (2 skipped, unchanged), up from 682. `flutter analyze`: 1 issue, identical
|
||
to baseline. Literal-encoding scan: zero hits.
|
||
|
||
**`size:exception` recorded.** Realized: **1,871 changed lines** (1,793+/78-) across 25 files against the 400-550
|
||
forecast — same "a strict-TDD commit carries its test files, and any ARB touch drags 13 generated files with it"
|
||
pattern as every prior WU (Engram `reference/estimating-strict-tdd-diffs`, id 2514). Breakdown: `lib/` production
|
||
code alone is 628 lines (`pantalla_ajustes_ecualizador.dart` +326, `ecualizador_widget.dart` restyle ~151,
|
||
`estado_ecualizador.dart` +64, new `servicio_presets_personalizados.dart` +87) plus 22 ARB source lines — at the top
|
||
edge of the forecast band by itself given this WU also had to build out a screen body WU3a only stubbed; the 4
|
||
test files (2 new state/service-layer files plus the widget test and the extended screen test) add 675 lines, and
|
||
11 new ARB keys drag in 546 more via the 13 regenerated `lib/l10n/gen/*.dart` files. Justification: the restyle,
|
||
the new persistence service, the state-layer extension and the screen wiring (explainer/salida-activa/drill-down/
|
||
save-flow) are one cohesive vertical slice — WU13 must land as a whole before WU14 can reuse its editor component
|
||
by exact runtime type; splitting further would leave either an unstyled widget with no screen consumer or a screen
|
||
half-wired to a service that doesn't exist yet.
|
||
|
||
## 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**.
|
||
|
||
- [x] 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).
|
||
- [x] 14.2 RED — the 4 tool-tray tiles ("EQ propio", "Grabar", sleep-timer value, "Compartir") each open their own
|
||
bottom sheet (Compartir invokes directly — there is no sheet content for a share action).
|
||
- [x] 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`).
|
||
- [x] 14.4 GREEN — restructured `pantalla_reproductor.dart` onto `PluriPushScaffold` (design ADR-2 — this screen is
|
||
the documented single consumer of `titleOverride`, a centered live/not-playing `PluriStatusPill`, and of the
|
||
non-default `leadingIcon: keyboard_arrow_down_rounded`): square art (`ClipRRect`, was `ClipOval`), favorite
|
||
moved into the transport row (the old redundant live-indicator dot removed — the AppBar pill already covers
|
||
that signal, so "favorite / stop / play-pause" matches the proposal's own "3 controles" note), single
|
||
subtitle line (collapses the old `_InfoChips` `Wrap`; codec/bitrate moved to the new quality row), 4-tile
|
||
tool-tray row replacing the always-expanded recording panel + separate sleep-timer button + EQ toggle. Body
|
||
wrapped in `SingleChildScrollView` (was a fixed `Column` + `Spacer()`, which overflowed even a generously
|
||
tall test viewport — see 2 bugfixes below).
|
||
- [x] 14.5 GREEN — wired "EQ propio" to a bottom sheet hosting `EcualizadorWidget` bound via
|
||
`EstadoEcualizador.presetParaEmisora(uuid)` / `guardarPresetPorEmisora(uuid, ...)`. Deliberately just the 5
|
||
sliders, no preset-chip row — the spec scenario's own wording is "5 sliders", and ADR-5's wiring table only
|
||
names `EcualizadorWidget` for this consumer.
|
||
- [x] 14.6 GREEN — added the quality row + "Cambiar" action and the "Compartir" tool-tray tile. **Design decision,
|
||
not specified by any ADR (WU14 has none)**: this app has no per-station alternate-quality capability to
|
||
invoke (a Radio Browser station is one fixed stream) — "Cambiar" reconnects the current stream (the SAME
|
||
`estado.reproducir(emisora)` call the existing error-state "Retry" button already uses) instead of a dead
|
||
button or an invented picker, matching this branch's "don't invent a capability absent from the domain"
|
||
discipline (WU5 per-station artwork, WU9 dashed border). "Compartir" mirrors WU15's injectable `compartir`
|
||
constructor-parameter pattern (defaults to the real `share_plus` call), sharing the station name + stream url.
|
||
- [x] 14.7 REFACTOR — confirmed no second EQ editor file was created (`grep`-equivalent: only one `EcualizadorWidget`
|
||
class exists, in `lib/widgets/ecualizador_widget.dart`, imported and reused here); confirmed the
|
||
`multi-device-eq` regression scenarios in `estado_ecualizador_test.dart`'s "4-level resolution (Phase 5)"
|
||
group still pass unmodified (this WU never touches `EstadoEcualizador`'s resolution logic, only calls its
|
||
EXISTING `presetParaEmisora`/`guardarPresetPorEmisora` methods). Removed dead code found during the
|
||
restructure: `_pulseController` (an `AnimationController` created and disposed but never actually driven by
|
||
anything) and the `SingleTickerProviderStateMixin` it required.
|
||
- [x] 14.8 Verify — the 3 EQ test files remain green and **unmodified**; `EcualizadorWidget` type-identity assertion
|
||
passes (both via `find.byType` and a `runtimeType`-predicate structural regression guard). Full suite:
|
||
730/730 green (2 skipped, unchanged), up from 713.
|
||
|
||
**Two pre-existing bugs found and fixed, surfaced by writing this screen's first-ever test coverage** (both
|
||
directly blocked test coverage from working at all, so neither could be deferred):
|
||
1. `initState` called `estado.reproducir(...)` directly, which calls `notifyListeners()` **synchronously** before
|
||
its first `await` when no recording needs stopping — threw "setState() or markNeedsBuild() called during
|
||
build" the instant this screen mounted against a fresh Provider tree. Fixed via `WidgetsBinding.instance.
|
||
addPostFrameCallback`.
|
||
2. The body `Column` had no scrollable ancestor and overflowed the default 800x600 test viewport (and would
|
||
overflow on a genuinely short real device too, given the content: hero, name, subtitle, quality row, visualizer,
|
||
transport, tool tray). Fixed by wrapping the body in `SingleChildScrollView` (see 14.4) — a real UX improvement,
|
||
not just a test workaround.
|
||
|
||
**`size:exception` recorded.** Realized: **1,335 changed lines** (1,114+/221-) across 19 files against the
|
||
450-600 forecast — same "a strict-TDD commit carries its test files" pattern as every prior WU (Engram
|
||
`reference/estimating-strict-tdd-diffs`, id 2514), though smaller this time since only 2 new ARB keys were needed
|
||
(`playerToolEqLabel`, `playerQualityChangeAction` — everything else reused existing keys: `recordAction`,
|
||
`recordingActiveTitle`, `sleepTimer`, `recordingActionShare`, `liveNow`, `notPlaying`, `qualityOriginal`,
|
||
`qualityUnknown`). Breakdown: `lib/pantallas/pantalla_reproductor.dart` alone is 658 lines (a near-total
|
||
restructure of a 907-line file, not a small patch); the new `pantalla_reproductor_test.dart` (writing coverage for
|
||
a file that had ZERO before this commit, per this WU's own explicit mandate) is 519 lines; `test/helpers/fakes.dart`
|
||
gained 64 (new `FakeServicioGrabacionRadioActivable` plus a `togglePlay()` override the play/pause characterization
|
||
test needed); the rest is the 2-key ARB/l10n-gen cascade. Not splittable: the restructure, the tool tray, and the
|
||
EQ-sheet wiring are one cohesive change to one screen — a split would leave either an unstyled screen with a tool
|
||
tray that has nothing to open, or a tool tray with no restructured screen to live in.
|
||
|
||
## 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.
|
||
|
||
## WU16 — Connectivity banner restyle (offline / reconnect)
|
||
|
||
**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.
|
||
|
||
- [x] 16.1 Verify-first — run `servicio_audio_reconnect_test.dart` and inspect the reconnect controller: confirm
|
||
whether it tracks an attempt count. **Result: it does NOT reach the UI.** `ControladorReconexion.intentos`
|
||
exists as a getter, but `ServicioAudio` (`lib/servicios/servicio_audio.dart`) only ever reads it inside a
|
||
`developer.log` debug line (line 413) — its public `estadoStream` maps every playback state to the
|
||
`EstadoReproduccion` enum (no attempt-count payload) and `_handler.reconectando` is a bare bool. Wiring a
|
||
count to the UI would require adding a getter/stream to `servicio_audio.dart`, which is one of the four
|
||
files this change must keep at an **empty git diff vs `main`** — structurally blocked, not just untested.
|
||
Shipping without the counter per 16.4, exactly as the risk register anticipated.
|
||
- [x] 16.2 RED — 3 new `testWidgets` cases added to `reconnect_ui_test.dart` (no attempt-count test, per 16.1):
|
||
reconnecting tints the spinner + status label with `offlineAccent`; plain `cargando` keeps the default
|
||
spinner colour (regression guard proving the tint is reconnect-specific, not blanket-loading); error tints
|
||
the retry icon + status label with `offlineAccent`. Confirmed RED: 2 of the 3 failed (`Actual: <null>`)
|
||
against the pre-restyle widget.
|
||
- [x] 16.3 GREEN — `lib/widgets/mini_reproductor.dart`: the status-label `Text` and the reconnect spinner /
|
||
error retry icon now read `context.pluriTokens.offlineAccent` (WU1's previously-unused token, whose own
|
||
doc comment already named it for this WU) whenever the stream reports `reconectando` or `error`. Plain
|
||
`cargando` explicitly keeps `color: null` (the theme default) — verified by 16.2's regression test.
|
||
- [x] 16.4 GREEN — conditional step confirmed moot: no attempt-count label added, no new plumbing introduced.
|
||
- [x] 16.5 REFACTOR — `TarjetaEmisoraShimmer` (`lib/widgets/tarjeta_emisora.dart`, consumed by
|
||
`pantalla_buscar.dart`) is untouched by this commit — confirmed via `git diff --stat`, zero lines.
|
||
- [x] 16.6 Verify — scoped suite green: 16/16 (5 `reconnect_ui_test.dart` + 8 `servicio_audio_reconnect_test.dart`
|
||
[byte-identical, unmodified] + 3 `mini_reproductor_configurar_test.dart` re-run as an adjacent-file
|
||
regression check). `flutter analyze`: 1 issue, identical to baseline. Scoped `dart format`: reformatted the
|
||
hand-written production file once (whitespace only), stable on re-run. Literal-encoding scan: zero hits on
|
||
the 2 touched files.
|
||
|
||
## 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`
|
||
|
||
- [x] 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. **Discovered at apply time**: a pre-existing,
|
||
UNRELATED `PluriOnboardingDialog` (a "what's new"/help-content modal loaded from markdown assets, shown from
|
||
`app.dart`'s launch flow) already exists — it is NOT the mockup's welcome screen and is not touched or
|
||
replaced by this WU.
|
||
- [x] 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.
|
||
- [x] 17.3 RED — tapping "Empezar a escuchar" lands on the Escuchar tab (index 0) with the welcome route removed
|
||
from the back stack. Test starts `EstadoNavegacionRaiz` on a DIFFERENT tab first so a pass actually proves
|
||
`irA()` ran, not just that escuchar was already the default.
|
||
- [x] 17.4 GREEN — built `lib/pantallas/pantalla_bienvenida.dart` as a full-screen route (bare `Scaffold`, no
|
||
`PluriPushScaffold` — this is an entry screen with no back affordance, not a second-level pushed screen);
|
||
content only (logo / headline / body / 3 bullets / 1 CTA) — no PRO pill, no trial duration, no price, no
|
||
secondary link. Content is the mockup's screen 14 copy verbatim MINUS its entire monetization block (the "14
|
||
días PRO gratis / luego 2,99 €/año" pricing card and the "Seguir con la versión gratuita" link), Spanish
|
||
re-cast to the app's established voseo register (the raw mockup HTML uses "tú" form; the other ~750 lines of
|
||
`app_es.arb` consistently use voseo — e.g. "Elegí", "Guardá", "querés" — matched that instead of copying the
|
||
mockup's grammar verbatim) and "auto" instead of the mockup's "coche" (matching the one existing precedent at
|
||
`localMusicSectionDescription`). The 3 bullet icon colours reuse existing named tokens
|
||
(`electricMagenta`/`liveGreen`/`warmCoral`) that happen to be the exact hex values the mockup already used for
|
||
those 3 icons — no new raw `Color(0x...)` literal introduced. `SingleChildScrollView` used instead of a
|
||
`Column` + `Spacer()` (the exact overflow hazard WU14 already hit and fixed) since this screen carries
|
||
meaningfully more content than a typical card. 9 new ARB keys, en/es only (`welcomeHeadline`, `welcomeBody`,
|
||
`welcomeBullet{1,2,3}{Title,Subtitle}`, `welcomeCtaLabel`).
|
||
- [x] 17.5 GREEN — CTA wired to `context.read<EstadoNavegacionRaiz>().irA(RaizPluriWave.escuchar)` then
|
||
`Navigator.of(context).pop()`. **Scope note**: this WU's own verify command and task list only cover
|
||
`pantalla_bienvenida_test.dart` — wiring the screen into the REAL first-launch flow (i.e. `app.dart`/
|
||
`main.dart` deciding *when* to push it, analogous to `ServicioContenidoApp.debeMostrarInicio()`'s pattern for
|
||
`PluriOnboardingDialog`) is NOT part of WU17 as scoped and was deliberately not added here to avoid inventing
|
||
untested persistence/launch semantics. Flagged as a likely WU17b gap (same shape as WU15/WU15b).
|
||
- [x] 17.6 REFACTOR — whole-repo grep confirmed clean: `\bPRO\b` (word-boundary) has exactly one hit in all of
|
||
`lib/` — this WU's own doc comment describing the guard (never rendered). The unqualified substring "PRO"
|
||
also matches pre-existing, unrelated `"PROGRAMADOS"` (vacation-alarms section title, WU9) and
|
||
`pluri_premium_widgets.dart` (a pre-existing decorative-hero-card widget file name, "premium" as in visual
|
||
styling, not a paid tier) — both false positives for a naive substring check, neither a monetization string.
|
||
No day-count trial phrase, no €/$ amount, no "gratuita"/"gratis"/"free version" string anywhere in
|
||
`lib/l10n/app_*.arb` or `lib/`.
|
||
- [x] 17.7 Verify — all 3 scenario tests green; grep scan clean.
|
||
|
||
## WU17b — Wire the welcome screen into the first-launch flow
|
||
|
||
**Not in the original plan — added to close the WU17 gap noted above, same shape as WU15/WU15b.** WU17 built
|
||
`PantallaBienvenida` fully tested and committed, but left it unreachable from the app: `rg "PantallaBienvenida"
|
||
lib/app.dart lib/main.dart` found nothing. This work unit exists solely to fix that.
|
||
|
||
**Commit**: `fix(bienvenida): wire the welcome screen into the first-launch flow`
|
||
**Depends on**: WU17
|
||
**Spec refs**: `onboarding-welcome` — Full-Screen Welcome Route (reachability; the render/content/no-monetization/CTA
|
||
scenarios stay WU17's own, unmodified)
|
||
**Verify**: `flutter test test/servicios/servicio_bienvenida_test.dart test/pantallas/pantalla_bienvenida_primer_lanzamiento_test.dart test/pantallas/pantalla_bienvenida_test.dart test/widget_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --cached --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||
**New tests**: `test/servicios/servicio_bienvenida_test.dart`, `test/pantallas/pantalla_bienvenida_primer_lanzamiento_test.dart`
|
||
**Modified tests**: none (WU17's own `pantalla_bienvenida_test.dart` re-run unmodified as a regression check)
|
||
|
||
**Coexistence ruling applied.** `PluriOnboardingDialog` (an unrelated, pre-existing "what's new"/help-content modal
|
||
loaded from markdown assets) is NOT deleted, merged, or replaced — first-launch welcome and a what's-new modal are
|
||
different things, confirmed by reading `assets/content/onboarding/en.md`: it is a detailed feature-reference
|
||
walkthrough plus per-version update notes, structurally different content from the welcome screen's 3-bullet
|
||
marketing intro. Both now run from `app.dart`'s `_PaginaPrincipalState`, in this order on every cold start: the
|
||
welcome screen's once-ever check resolves FIRST, then the pre-existing what's-new dialog's own independent
|
||
per-version due-or-not check runs exactly as it did before this WU. Sequencing (not two independent fire-and-forget
|
||
calls) is what prevents the two from ever racing onto the screen at the same time.
|
||
|
||
- [x] 17b.1 RED — `test/servicios/servicio_bienvenida_test.dart`: a new `ServicioBienvenida` (mirroring
|
||
`ServicioContenidoApp`'s injectable-`SharedPreferences`, versioned-key convention, S3-R4) is due before it has
|
||
ever been marked seen, is not due after `marcarBienvenidaVista()`, and respects a seen flag already persisted
|
||
by a prior launch (`SharedPreferences.setMockInitialValues`).
|
||
- [x] 17b.2 RED — `test/pantallas/pantalla_bienvenida_primer_lanzamiento_test.dart`: a new
|
||
`PantallaBienvenida.mostrarSiProcede(context)` static method (mirroring `PluriOnboardingDialog.mostrarSiProcede`'s
|
||
check-then-show-then-mark-seen shape) pushes the welcome screen on a first launch (no seen flag persisted), does
|
||
NOT push it when the seen flag is already persisted, and — in one continuous session — persists the flag after
|
||
being shown once so a later check in the same run skips it.
|
||
- [x] 17b.3 GREEN — created `lib/servicios/servicio_bienvenida.dart` (`ServicioBienvenida`, key
|
||
`pluri_bienvenida_vista_v1`, plain one-time boolean — no version comparison needed, unlike
|
||
`ServicioContenidoApp`, since this welcome is a single first-impression surface, not a per-version one).
|
||
- [x] 17b.4 GREEN — added the static `PantallaBienvenida.mostrarSiProcede(BuildContext)` method to the existing
|
||
`pantalla_bienvenida.dart` file (no new wrapper class needed — unlike `PluriOnboardingDialog`, there is only one
|
||
call shape here); updated the class doc comment to describe how it coexists with `PluriOnboardingDialog`.
|
||
- [x] 17b.5 GREEN — wired `app.dart`: imported `pantalla_bienvenida.dart`; renamed the existing
|
||
`_onboardingInicialSolicitado` guard flag to `_flujoPrimerLanzamientoSolicitado` (it now covers the combined
|
||
sequence, not only the dialog); added `_mostrarFlujoPrimerLanzamiento()`, which awaits
|
||
`PantallaBienvenida.mostrarSiProcede(context)` then calls the pre-existing, untouched
|
||
`_mostrarOnboardingInicial()` — replacing the single `unawaited(_mostrarOnboardingInicial())` call site with
|
||
`unawaited(_mostrarFlujoPrimerLanzamiento())`.
|
||
- [x] 17b.6 REFACTOR — confirmed `_mostrarOnboardingInicial()`'s own body (900ms delay, `_alarmaSonandoActiva` guard,
|
||
`PluriOnboardingDialog.mostrarSiProcede` call) is byte-for-byte unchanged — only its call site moved one level
|
||
deeper into the new sequencing method. No scenario in `PantallaBienvenida`'s own WU17 test file needed to
|
||
change (all 3 pass unmodified, confirming the CTA/content/no-monetization behavior is untouched).
|
||
- [x] 17b.7 Verify — scoped suite green: 13/13 (3 `servicio_bienvenida_test.dart` + 3
|
||
`pantalla_bienvenida_primer_lanzamiento_test.dart` + 3 `pantalla_bienvenida_test.dart` [byte-identical,
|
||
unmodified] + 4 `widget_test.dart`). `flutter analyze`: 1 issue, identical to baseline. Scoped `dart format`:
|
||
reformatted 1 of 5 touched files (whitespace-only string-literal wrapping), stable on re-run. Literal-encoding
|
||
scan: one console-rendering false positive on the pre-existing "días" string (verified byte-correct UTF-8 via a
|
||
direct file read with the encoding pinned), zero real corruption.
|
||
|
||
## 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.
|