docs(sdd): add functional redesign proposal
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
# Proposal: Functional Redesign (`rediseno-funcional`)
|
||||
|
||||
> Source of truth: `rediseno-funcional-de-aplicacion/project/PluriWave Rediseno.dc.html`, turn `t4` only.
|
||||
> Binding inputs: Engram `sdd/rediseno-funcional/scope-decisions`, `sdd/rediseno-funcional/open-questions-resolved`,
|
||||
> `sdd/rediseno-funcional/eq-band-spike` (id 2498), `reference/radio-browser-countries-endpoint` (id 2500),
|
||||
> `preferences/verify-external-apis-officially` (id 2501), `explore-mobile`, `explore-auto`, `explore-crosscutting`.
|
||||
> Do not re-open decisions recorded there.
|
||||
> This artifact intentionally exceeds the default 450-word proposal budget: the change spans 14 mobile screens,
|
||||
> 5 Android Auto surfaces, and 18 work units, and the delivery/risk sections are load-bearing for the apply phase.
|
||||
|
||||
## Intent
|
||||
|
||||
**Problem.** The app's current UI is a glass-everywhere magenta/coral surface built up incrementally over many
|
||||
changes. Three concrete symptoms:
|
||||
|
||||
1. **Settings has collapsed under its own weight.** `lib/pantallas/pantalla_ajustes.dart` is 1896 lines rendering
|
||||
**12 always-expanded panels** on one scroll. The mockup's "12 panels to 4 groups" framing describes the current
|
||||
app *exactly* — the redesign premise is verified, not aspirational.
|
||||
2. **The home tab does not do its job.** `pantalla_inicio.dart` renders a discovery browser (near-you carousel,
|
||||
genre chips, trending grid) while the thing users open the app for — the player and their own stations — is
|
||||
one tap away behind a pushed route.
|
||||
3. **The visual language drifted from its own tokens.** The mockup's `#07121A` base and `#21D4D9` brand already
|
||||
match `PluriWaveTokens`, but the list surface `#102532`, the live green `#7EE4C2`, and the offline accent are
|
||||
used ad-hoc across screens, and `.copyWith(fontWeight: w900, letterSpacing: ...)` is copy-pasted in nearly every
|
||||
screen file. There is no named type scale.
|
||||
|
||||
**Why now.** The design handoff (turn `t4`) is approved and the app is in Google Play internal testing — the
|
||||
lowest-risk window to restructure navigation-adjacent UI before a public audience exists. Version is already
|
||||
bumped to `1.2.0+122`; `17f8e69` on `main` is a clean rollback point.
|
||||
|
||||
**Success looks like.** Every mockup screen's *intent* is implemented in the new visual language, information
|
||||
architecture is unchanged (5 tabs), no existing capability is lost, and no monetization surface ships.
|
||||
|
||||
## Standing Rule — external APIs are verified, never inferred
|
||||
|
||||
**Any technical doubt about connectivity to an external API or service MUST be resolved against that provider's
|
||||
official documentation. Inference from existing call sites, from field names, or from what "looks right" is not
|
||||
acceptable evidence.** This binds `sdd-spec`, `sdd-design`, and `sdd-apply` for this change and every change after
|
||||
it. Engram: `preferences/verify-external-apis-officially` (id 2501).
|
||||
|
||||
Practical consequence here: no spec, design, or task may assert a Radio Browser endpoint, parameter, or response
|
||||
field that has not been checked against <https://api.radio-browser.info/>. Where such a check has already been
|
||||
done, the verified contract is recorded below and is authoritative.
|
||||
|
||||
## Reconciliation Ruling — mockup push-chrome vs. 5 tabs
|
||||
|
||||
The mockup states (t4 intro, line 38): *"cuatro raíces con barra globo … las pantallas de segundo nivel entran
|
||||
empujadas con cabecera de 56px, flecha atrás y SIN barra de pestañas."*
|
||||
|
||||
**Ruling: adopt the push-chrome rule, reject the root set.**
|
||||
|
||||
- Every genuine second-level screen gets 56px header + back arrow + **no** tab bar: Reproductor completo (2),
|
||||
Países (5), Vacaciones (9bis), Ecualizador (11), Grabaciones (12), and all new Settings detail screens.
|
||||
- All **five** roots keep the tab bar: Escuchar, Buscar, Favoritos, Alarmas, Ajustes.
|
||||
- **The one deliberate contradiction**: mockup screen 4 ("Tus emisoras · grupos") is *drawn* as a pushed screen with
|
||||
no tab bar. In our IA it **is** the Favoritos root, so it **keeps its tab bar**.
|
||||
|
||||
**Reasoning.** The mockup's no-tab-bar rule is a *consequence* of its 4-root IA, not an independent design law. Its
|
||||
real intent is "navigation depth removes the root switcher." Applying it literally to Favoritos would hide the tab
|
||||
bar on a root tab — a navigation regression, not a redesign. The rule is honoured at the level of intent (depth
|
||||
implies no tabs) and rejected at the level of the specific frame that only looked pushed because the mockup had
|
||||
demoted Favoritos out of the root set. Downstream phases must not copy "no tab bar" onto Favoritos.
|
||||
|
||||
**Corollary ruling (not covered by the 8 resolutions):** the mockup's 4 Settings groups draw exactly 16 rows,
|
||||
omitting Sleep Timer and Backup/restore. Per the standing "do not silently narrow scope" rule, **both are kept**
|
||||
under the `APLICACIÓN` group. Sleep timer stays reachable from both Settings and the player tool tray.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
Ordered as work units (WU). This ordering is the dependency graph, not a suggestion.
|
||||
|
||||
| WU | Deliverable | Depends on | Est. lines | Risk |
|
||||
|----|-------------|-----------|-----------|------|
|
||||
| WU1 | **Design tokens + shared primitives + the Inicio-to-Escuchar rename**: add `listSurface #102532`, `liveGreen #7EE4C2`, `offlineAccent #E8879A`; named text styles (`heroTime`, `eyebrowLabel`, `cardTitle`, `screenTitle`); shared push-chrome scaffold (56px header + back, no tab bar); **retitle the first tab in `lib/app.dart` and update the `navHome` value in `app_en.arb` + `app_es.arb`** (see rename ruling below) | — | 160-260 | Low |
|
||||
| WU2 | **Android Auto verification-only**: re-run `test/servicios/navegacion_auto_test.dart`, formally close A1-A5 as already-done. **Zero code changes.** | — | 0 | None |
|
||||
| WU3a | **Settings shell + AUDIO/EMISORAS groups**: 12 panels to grouped nav rows, extract detail screens, controls reused verbatim | WU1 | 350-450 | Low (mechanical) |
|
||||
| WU3b | **Settings GRABACIONES Y MÚSICA / APLICACIÓN groups** (incl. sleep timer + backup) | WU3a | 350-450 | Low (mechanical) |
|
||||
| WU4 | **Favoritos restyle**: chip group filter replaces stacked panels, flat reorderable list (drag-to-reorder is NEW), `swap_vert` sort, custom-station CTA | WU1 | 300-400 | Medium |
|
||||
| WU5 | **Escuchar restructure**: embedded player hero (square art, live pill, waveform, transport row with sleep as 5th action, tool-tray entry chip) + "Tus emisoras" favorites grid + "Ver todas" switching to the Favoritos tab (index switch, not push) | WU1, WU4 | 350-450 | Medium-High |
|
||||
| WU6 | **Buscar**: discovery content (near-you / genres / trending / countries entry) becomes the tab's landing state; active-filter pills with bottom-sheet pickers; results counter; one-tap "clear N filters". Opens with an **official-docs check** on Radio Browser sort support (resolution 8, still open) | WU5 | 350-500 | Medium |
|
||||
| WU7 | **Países browser (NEW screen)**: "Tus idiomas" + full alphabetical list with per-country counts, over the **verified** `/json/countries` contract below. Includes the `_get` transport extraction in `servicio_radio.dart` **plus regression coverage for the 8 existing station calls** | WU6 | 300-400 | Medium |
|
||||
| WU8 | **Alarmas root restyle**: minimal cards (giant time + station + switch), **tap = edit, swipe = delete** (resolution 2), hero banner with inline "Saltar", Vacaciones inline panel becomes a summary row + chevron | WU1 | 400-550 | Medium |
|
||||
| WU9 | **Vacaciones manager (NEW screen 9bis)**: active-range hero, per-alarm impact line, upcoming ranges, past-ranges history. Needs new `EstadoAlarmas` query methods | WU8 | 350-450 | Medium-High |
|
||||
| WU10 | **Alarm editor sheet rewrite**: inline giant HH:MM editor (custom widget), always-visible weekday circles, restyled sliders. **Keeps** the date field, fallback-station picker, and sound dropdown (resolution 3), collapsed into an advanced section | WU8 | 500-650 | High |
|
||||
| WU11 | **Alarm ringing restyle**: full-bleed blurred art, giant time, 3 snooze tiles, full-width stop pill. **Static** "Subiendo volumen" label with **no** seconds counter (resolution 4). Force-stop retry banner and single-exit dismiss guard preserved verbatim | WU1 | 200-300 | High (safety-critical) |
|
||||
| ~~WU12~~ | ~~Native EQ band-count spike~~ — **RESOLVED before planning closed.** Tombstone row: the number is retired, not missing. Evidence in Engram id 2498, ruling below | — | 0 | — |
|
||||
| WU13 | **Ecualizador settings screen — restyle at 5 bands**: header enable toggle, base-vs-per-station explainer, preset chips, **5** custom vertical sliders (mockup's 7 rejected — see below), "Guardar como preset" (NEW custom presets), "Salida activa" surfaced from Advanced, "Emisoras con ajuste propio" drill-down (NEW UI over existing `presetsPorEmisora`). **No model, persistence, or EQ-test change from the band axis.** | WU3a | 400-550 | Low-Medium |
|
||||
| WU14 | **Reproductor completo restructure**: square art, favorite moved into transport row, single subtitle line, 4-tile tool-tray grid (EQ propio / Grabar / sleep-timer / Compartir) opening bottom sheets, quality row + "Cambiar". Per-station EQ sheet **shares** the WU13 editor component | WU13 | 450-600 | Medium-High |
|
||||
| WU15 | **Grabaciones library (NEW list)**: storage usage bar, per-recording rows with playback, `⋮` menu = **Rename / Share / Delete** constrained to what `servicio_grabacion_radio.dart` already exposes (resolution 6) | WU3b | 300-400 | Medium |
|
||||
| WU16 | **Connectivity states**: offline banner restyle, reconnect attempt counter (**verify** the reconnect controller exposes an attempt count before promising the label), shimmer skeletons already correct | WU1 | 150-250 | Low |
|
||||
| WU17 | **Welcome / onboarding screen (14)**: full-screen route in the new visual language — logo, headline, body, 3 feature bullets, "Empezar a escuchar" CTA. **No PRO pill, no "14 días", no pricing card, no secondary "free version" link** (resolution 7) | WU1 | 150-200 | Low |
|
||||
| WU18 | **i18n batch**: owns **both new keys AND modified values for existing keys**. ~35-45 new keys across all 13 ARB locales (~455-585 translated strings), **plus the `navHome` value in the 11 locales WU1 did not touch**. Run **once**, after WU1-WU17 land in `en`/`es` | all | 0 eng. | Low |
|
||||
|
||||
Total estimated changed lines: **~4,550-6,450**, excluding tests.
|
||||
|
||||
**Resolved ruling — the Inicio-to-Escuchar rename changes the ARB *value*, not the key.**
|
||||
|
||||
Verified directly: `navHome` sits at **line 4 of all 13 ARB files** (`app_en.arb` = `"Home"`, `app_es.arb` = `"Inicio"`,
|
||||
and 11 further localized values — `Beranda`, `Accueil`, `Start`, `Início`, `Inizio`, `ホーム`, `首页`, `Главная`,
|
||||
`الرئيسية`, `হোম`, `मुखपृष्ठ`). So this is a **value change to an existing key**, not a new key — which is why neither
|
||||
WU1's original scope nor WU18's "new keys" scope covered it. Both are corrected above.
|
||||
|
||||
Consequence for WU18: this is **13 real translations of "Listen"**, not a find-and-replace. Only `en` and `es` are
|
||||
mechanical; the other 11 need a translator, exactly like any new key.
|
||||
|
||||
| Option | Ruling |
|
||||
|--------|--------|
|
||||
| Change `navHome`'s **value** in place | **ADOPTED.** 13 value edits, zero key churn, zero call-site churn, and no window where a locale can drift out of key-set parity mid-refactor. |
|
||||
| Introduce `navListen`, retire `navHome` | Rejected. |
|
||||
|
||||
Reasoning for the rejection: binding decision 2 keeps `PluriIconGlyph.home` as the Escuchar tab icon. "Home" therefore
|
||||
already survives as this tab's internal identifier **by explicit user decision**. Renaming the ARB key to `navListen`
|
||||
while the icon glyph stays `home` would make internal naming *less* consistent, not more, and would drag every call
|
||||
site and any key-asserting test along for no user-visible gain.
|
||||
|
||||
Ownership split (so no two units edit the same line): **WU1** changes the value in `app_en.arb` and `app_es.arb` — the
|
||||
two locales every slice lands in per approach point 9. **WU18** carries it to the remaining 11 locales alongside the
|
||||
new-key batch.
|
||||
|
||||
**Resolved ruling — equalizer stays at 5 bands.** The mockup's 7-band layout (t4, screen 11) is **rejected as not
|
||||
reliably implementable**. Spike evidence (Engram `sdd/rediseno-funcional/eq-band-spike`, id 2498):
|
||||
|
||||
- There is **no native Kotlin equalizer code at all** — zero MethodChannel/EventChannel EQ plumbing under
|
||||
`android/app/src/main/kotlin`. The Kotlin side holds only the waveform downsampler and the alarm service.
|
||||
- Equalization runs through `just_audio`'s `AndroidEqualizer`, which wraps the Android system `AudioEffect`
|
||||
Equalizer. **Band count comes from `params.bands`, reported by the device/OS — the app does not choose it.**
|
||||
Most Android devices report exactly 5.
|
||||
- `lib/servicios/servicio_audio.dart:749-762` is the only place gains are applied and already guards with
|
||||
`i < params.bands.length && i < preset.bandas.length`. Rendering 7 sliders would **silently no-op the last two**
|
||||
on typical hardware — shipping controls that do nothing.
|
||||
- `lib/modelos/preset_ecualizador.dart:5-8` hard-asserts `bandas.length == 5`.
|
||||
|
||||
Consequence: WU13 is a **restyle only**. No model change, no persistence change, no EQ test churn from this axis.
|
||||
|
||||
**Verified contract — Radio Browser countries (WU7).** Checked against <https://api.radio-browser.info/> and
|
||||
<https://de1.api.radio-browser.info/>. Full detail: Engram `reference/radio-browser-countries-endpoint` (id 2500).
|
||||
This closes half of open question 8. Downstream phases use these facts verbatim — do not re-derive them.
|
||||
|
||||
- Endpoint: **`/json/countries`**, optional substring filter `/json/countries/{filter}`.
|
||||
**`/json/countrycodes` is DEPRECATED — do not use it.**
|
||||
- Response fields are exactly `name` (string), `iso_3166_1` (string), and **`stationcount` (STRING, not int)**.
|
||||
An `as int` cast throws at runtime. **Parse with `int.tryParse`.**
|
||||
- Params/defaults: `order` (default `name`, also accepts `stationcount`), `reverse` (false), `hidebroken` (false),
|
||||
`offset` (0), `limit` (100000).
|
||||
|
||||
Two implementation constraints this imposes:
|
||||
|
||||
1. **`lib/servicios/servicio_radio.dart:168` hardcodes `lastcheckok: '1'` into every request built by `_get`.**
|
||||
That is a **station-only** filter and must NOT be sent to `/json/countries`. The countries call therefore
|
||||
**cannot reuse `_get` as-is**.
|
||||
2. **Required shape**: extract the transport loop from `_get` (server discovery, host rotation, bounded retries,
|
||||
User-Agent, timeout, status check, `json.decode`) into a generic private helper returning `List<dynamic>`.
|
||||
`_get` then layers the station-specific concerns on top (`lastcheckok`, `Emisora.fromApi`, `_compararCalidad`
|
||||
sort); the countries call reuses **transport only**. No behaviour change for the 8 existing methods — but this
|
||||
touches a **shared code path**, so WU7 must ship regression coverage for those 8 calls.
|
||||
|
||||
Already correct, needs no work: `_uri` (lines 103-105) forces `hidebroken=true` on every request, which is exactly
|
||||
what we want given the endpoint default of `false`; mirror discovery via `all.api.radio-browser.info` matches the
|
||||
documented guidance; the User-Agent is built per instance at lines 68-86.
|
||||
|
||||
Still open under question 8: the **search sort** control (screen 6). Not yet verified — WU6 opens with an official-
|
||||
docs check before anything is specced.
|
||||
|
||||
### Out of Scope — explicitly
|
||||
|
||||
| Excluded | Why |
|
||||
|----------|-----|
|
||||
| **Monetization: PRO tier, 14-day trial, pricing card, IAP, subscription state, feature gating, the Settings "PRO · 11 meses" pill** | User decision (resolution 7). Zero monetization infrastructure exists today; `in_app_purchase` stays commented out at `pubspec.yaml:56`. Ships as its own SDD change at public release. Do not display a paid-tier promise the app cannot honour. |
|
||||
| **Alarm-ringing live volume countdown ("Subiendo volumen · 18 s")** | Resolution 4. Requires a native to Flutter progress channel that is *deliberately* absent per a documented architecture constraint in `pantalla_alarma_sonando.dart`. Static label only. |
|
||||
| **Any literal Android Auto pixel work (A1-A5)** | The app uses the classic `MediaBrowserService` model (`automotive_app_desc.xml`, no `androidx.car.app`). Android Auto's system templates render everything; the only controllable surface is `MediaItem` metadata, which already matches. A4's waveform is **non-implementable** on this platform. WU2 is verification-only. |
|
||||
| Adopting the mockup's 4-root IA (Escuchar / Explorar / Alarmas / Ajustes) | Binding decision 1. Would delete the Favoritos root and demote the player to a pushed route. |
|
||||
| Switching tab icons to Material Symbols | Binding decision 2. `PluriIconGlyph.*` stays; `PluriIconGlyph.home` becomes the Escuchar icon. |
|
||||
| **The mockup's 7-band equalizer** | Rejected outright. Band count is device-reported via `just_audio`'s `AndroidEqualizer` (`params.bands`), not app-chosen; most devices report 5, so 2 of 7 sliders would silently do nothing. See the resolved ruling above. |
|
||||
| **A runtime-dynamic band count driven by `params.bands.length`** | Technically the only correct way to support non-5-band devices, but it ripples through the fixed-length `assert(bandas.length == 5)`, the 7 SharedPreferences keys in `servicio_ecualizador.dart`, and every EQ test. **Worth its own future change** — not this one. |
|
||||
| Domain/business-logic changes to the EQ resolution hierarchy | Already a 4-level matrix in `estado_ecualizador.dart`. The redesign **relocates editing surfaces only**. |
|
||||
|
||||
## Capabilities
|
||||
|
||||
> Contract with `sdd-spec`. Existing spec names verified against `openspec/specs/`.
|
||||
|
||||
### New Capabilities
|
||||
- `app-navigation-shell`: the 5-root tab contract, the push-chrome rule (56px header + back + no tab bar) and its explicit Favoritos exemption, and root-to-root switching (Escuchar "Ver todas" to Favoritos).
|
||||
- `station-discovery-browse`: discovery content as the Buscar landing state, active-filter pills, results counter, clear-all-filters, the Países browser over the verified `/json/countries` contract, and search sorting **only if** official docs confirm support.
|
||||
- `favorites-organization`: chip-filtered flat list, drag-to-reorder, sort action, group management surfaced from Favoritos.
|
||||
- `alarm-vacation-ranges`: active-range detection, per-alarm pause-impact computation, upcoming and past ranges.
|
||||
- `recordings-library`: browsable recordings list, storage usage, per-recording rename/share/delete.
|
||||
- `eq-custom-presets`: user-defined preset save/name/list, per-station EQ entry point from the player, "stations with their own EQ" drill-down.
|
||||
- `onboarding-welcome`: first-run welcome surface, explicitly monetization-free.
|
||||
|
||||
### Modified Capabilities
|
||||
- `multi-device-eq`: the per-station EQ editing surface moves into the player's tool tray and gains custom presets. **Band count stays 5 — resolved, not pending.**
|
||||
- `native-alarms`: alarm-card affordances change (tap = edit, swipe = delete), vacation ranges move to a dedicated screen, ringing-screen presentation changes. **Scheduling and dismiss-guard semantics are unchanged and must not regress.**
|
||||
- `android-auto-media`: **no requirement change** — verification only, listed here so `sdd-spec` records the confirmation rather than writing a delta.
|
||||
|
||||
## Approach
|
||||
|
||||
1. **Tokens before screens.** WU1 lands the named tokens and text styles first so the copy-paste
|
||||
`.copyWith(fontWeight: w900)` pattern is not re-applied 14 times and then refactored 14 times.
|
||||
2. **Cheap certainty first.** WU2 (Android Auto) costs nothing and formally closes 5 of the 19 mockup screens.
|
||||
3. **Mechanical before creative.** The Settings restructure (WU3a/WU3b) is the largest line count in the plan but
|
||||
the lowest logic risk — panels become nav rows, controls are moved verbatim. Landing it early proves the
|
||||
push-chrome primitive against real screens.
|
||||
4. **Restyle, do not re-architect, the EQ.** The band-count question is settled at 5. WU13 changes only the
|
||||
presentation layer; `preset_ecualizador.dart`, `servicio_ecualizador.dart`, and `servicio_audio.dart` are
|
||||
untouched by the band axis.
|
||||
5. **Share components, do not duplicate.** WU13 (Settings EQ editor) ships *before* WU14 (player tool tray) so
|
||||
the per-station EQ bottom sheet reuses the same editor widget.
|
||||
6. **Verify external APIs officially, then record the contract.** Per the standing rule above. The
|
||||
`/json/countries` contract is already verified and recorded; search sort is not, and WU6 starts by checking it.
|
||||
7. **Characterise before refactoring shared code.** WU7 writes regression tests for the 8 existing station calls
|
||||
*before* extracting transport out of `_get` — the refactor is only safe if the old behaviour is pinned first.
|
||||
8. **Strict TDD throughout.** Tests are written before implementation for every WU. `flutter test` is the runner.
|
||||
`flutter analyze` and `dart format .` gate every commit. **`flutter build` is never run.**
|
||||
9. **Translate once.** WU18 batches all new ARB keys at the end rather than paying 13-locale cost per slice.
|
||||
|
||||
## Affected Areas
|
||||
|
||||
| Area | Impact | Description |
|
||||
|------|--------|-------------|
|
||||
| `lib/tema/pluriwave_tokens.dart`, `pluriwave_theme.dart` | Modified | 3 new colour tokens, named text styles. No token removed, none changed. |
|
||||
| `lib/app.dart`, `lib/widgets/pluri_bottom_navigation.dart` | Modified | **WU1** retitles the first tab to `Escuchar`; `PluriIconGlyph.home` unchanged. Tab list otherwise stable. |
|
||||
| `lib/pantallas/pantalla_inicio.dart` (413 lines) | Restructured | Discovery content out, player hero + favorites grid in. |
|
||||
| `lib/pantallas/pantalla_buscar.dart` (357 lines) | Restructured | Gains discovery landing state, filter pills, counter, sort. |
|
||||
| `lib/pantallas/pantalla_favoritos.dart` (287 lines) | Restructured | Stacked panels to chip-filtered reorderable list. |
|
||||
| `lib/pantallas/pantalla_alarmas.dart` (1383 lines) | Restructured + split | Cards simplified; vacation panel extracted to its own screen; editor sheet rewritten. |
|
||||
| `lib/pantallas/pantalla_alarma_sonando.dart` (313 lines) | Restyled | Full-bleed layout. Dismiss guard + force-stop banner preserved. |
|
||||
| `lib/pantallas/pantalla_ajustes.dart` (1896 lines) | Split | 12 panels to 4 grouped nav lists + ~8 new detail screen files. |
|
||||
| `lib/pantallas/pantalla_reproductor.dart` (907 lines) | Restructured | Square art, transport row, 4-tile tool tray, share, quality switch. |
|
||||
| `lib/pantallas/` (new files) | New | Países, Vacaciones manager, recordings library, welcome, ~8 settings detail screens. |
|
||||
| `lib/modelos/preset_ecualizador.dart` | Modified | Custom presets only. `assert(bandas.length == 5)` **stays**. |
|
||||
| `lib/servicios/servicio_ecualizador.dart`, `lib/servicios/servicio_audio.dart` | **Unchanged** | EQ band handling and gain application are untouched — restyle only. |
|
||||
| `lib/estado/estado_alarmas.dart` | Modified | New queries: current range, per-alarm impact, past ranges. |
|
||||
| `lib/servicios/servicio_radio.dart` | Modified | Extract the transport loop out of `_get` into a generic helper (WU7). `_get` keeps `lastcheckok`/`Emisora.fromApi`/sort. **Shared code path — needs regression coverage for all 8 existing station calls.** |
|
||||
| `lib/estado/estado_busqueda.dart` | Modified | Country list + counts; sort parameter **only if** official docs confirm support (WU6). |
|
||||
| `lib/l10n/app_*.arb` (13 files) | Modified | ~35-45 new keys each, **plus modified values for existing keys — known: `navHome`** (WU1 does `en`/`es`, WU18 does the other 11). Key sets stay identical throughout. |
|
||||
| `lib/servicios/navegacion_auto.dart` | **Unchanged** | Verification only. |
|
||||
| `test/` (59 files, 15,143 lines) | Modified + New | See risk register. |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| ~~EQ 5 to 7 bands blocked by the native layer~~ — **RETIRED, resolved by spike (Engram id 2498).** Band count is device-reported via `params.bands`; 7 sliders would silently no-op on typical 5-band hardware. | — | — | **Closed.** Equalizer stays at **5 bands**; WU13 is a restyle. Residual risk moves to the row below. |
|
||||
| **Spec drift back toward 7 bands.** The approved mockup still *draws* 7 sliders, so a downstream phase could copy it from the design source without reading this ruling. | Medium | Medium | The 5-band ruling is stated in the WU table, the resolved ruling block, Out of Scope, Capabilities, and Success Criteria. Any spec or task asserting 7 bands is rejected on sight. `assert(bandas.length == 5)` in `preset_ecualizador.dart` is the compile-time backstop. |
|
||||
| **Non-5-band devices remain unsupported.** Devices whose OS reports 6+ bands get the extra bands left at default gain. This is pre-existing behaviour, not a regression introduced here. | Low | Low | Explicitly out of scope. A runtime-dynamic band count is its own future change — it touches the fixed-length assert, the 7 SharedPreferences keys in `servicio_ecualizador.dart`, and every EQ test. |
|
||||
| **`pantalla_ajustes.dart` (1896 lines) restructure.** Largest single extraction in the plan; ~8 new files. | High (it will happen) | Medium | Mechanical extraction — controls move verbatim, business logic untouched. Split across WU3a/WU3b so neither PR exceeds ~450 lines. `pantalla_ajustes_test.dart` gets a near-total rewrite, written first per strict TDD. |
|
||||
| **15,143 lines of existing tests assume the current structure.** Confirmed impacted: `pantalla_inicio_test`, `pantalla_inicio_rebuild_test`, `pantalla_favoritos_plural_test`, `pantalla_buscar_shimmer_test`, `pantalla_alarmas_editor_test`, `pantalla_alarmas_fecha_test`, `pantalla_ajustes_test`, `reconnect_ui_test`, `widget_test`. The three EQ test files (`servicio_ecualizador_test`, `estado_ecualizador_test`, `servicio_audio_eq_reapply_test`) are **no longer at risk** now that band count is fixed at 5. | High | Medium | Each WU owns its test updates; no WU merges with a red suite. **Hard rule: `pantalla_alarma_sonando_dismiss_guard_test.dart` must pass unmodified** — if a restyle requires changing it, stop and escalate, because that test guards a real safety mechanism. |
|
||||
| Reconnect attempt counter may not exist in the controller | Medium | Low | WU16 verifies `servicio_audio_reconnect_test.dart` first; if no attempt count is tracked, ship the banner restyle without the counter rather than adding plumbing. |
|
||||
| **Refactoring `_get` in `servicio_radio.dart` regresses the 8 existing station calls.** WU7 must split transport from station-specific concerns because `lastcheckok: '1'` (line 168) cannot be sent to `/json/countries`. | Medium | **High** — every station listing in the app flows through `_get` | Pure extraction, no behaviour change intended. WU7 ships **regression coverage for all 8 existing station calls first** (strict TDD: characterisation tests before the refactor). Server discovery, host rotation, retries, User-Agent and timeout move as one block — no logic edits during the move. |
|
||||
| **`stationcount` parsed as int and throws at runtime.** The `/json/countries` field is a **string**, which reads like an int. | Medium | Medium | Contract is recorded above and in Engram id 2500: parse with `int.tryParse`. Spec must state the field type explicitly; a fixture with a string `stationcount` is required in the WU7 tests. |
|
||||
| Search sort support is still unverified (resolution 8, second half) | Medium | Medium | WU6 opens with an **official-docs check**, not a code spike. Per the standing rule, no spec may assert a sort parameter that has not been confirmed at api.radio-browser.info. If unsupported, drop the "Ordenar" control rather than faking client-side ordering. |
|
||||
| **Downstream phases infer API behaviour instead of checking the docs.** | Medium | High | The standing rule is stated at the top of this proposal. `sdd-spec` and `sdd-design` must cite the official doc (or the recorded Engram contract) for every external-service claim. Uncited API claims are rejected. |
|
||||
| Custom inline time editor (WU10) is the largest genuinely-new widget | Medium | Medium | Isolated in its own PR, never bundled. Drag/tap adjust logic is unit-tested independently of the sheet. |
|
||||
| i18n volume: ~455-585 translated strings across 13 locales | High | Low (ops, not eng.) | WU18 batches once at the end. Slices land in `en`/`es` only. |
|
||||
| Scope creep back into monetization via the "PRO" pill | Low | High | Explicitly excluded above and in resolution 7. Any PR reintroducing it is rejected. |
|
||||
|
||||
## Delivery Shape
|
||||
|
||||
This change is roughly **4,550-6,450 changed lines excluding tests** — an order of magnitude beyond the 400-line
|
||||
review budget. A single PR is not reviewable.
|
||||
|
||||
**Recommendation: `feature-branch-chain`.**
|
||||
|
||||
- `feat/rediseno-funcional` is the tracker branch and the **only** branch that merges to `main`.
|
||||
- PR #1 targets `feat/rediseno-funcional`. Each subsequent child PR targets the immediately previous PR's branch,
|
||||
so review diffs stay scoped to one work unit. If GitHub shows earlier slices in a child diff, rebase/retarget
|
||||
until clean.
|
||||
- Rationale over `stacked-to-main`: this is a coordinated visual language change. Half-landed on `main` means a
|
||||
release where some screens use the new tokens and others do not. The tracker branch gives one atomic rollback.
|
||||
|
||||
**Sequence** (each row is one PR):
|
||||
|
||||
```
|
||||
PR1 WU1 tokens + push-chrome primitive (unblocks everything)
|
||||
PR2 WU2 Android Auto verification (zero code, quick win)
|
||||
PR3 WU3a Settings shell + AUDIO/EMISORAS
|
||||
PR4 WU3b Settings GRABACIONES/APLICACIÓN
|
||||
PR5 WU4 Favoritos restyle
|
||||
PR6 WU5 Escuchar restructure
|
||||
PR7 WU6 Buscar landing + filters (spike first)
|
||||
PR8 WU7 Países browser
|
||||
PR9 WU8 Alarmas root restyle
|
||||
PR10 WU9 Vacaciones manager
|
||||
PR11 WU10 Alarm editor sheet (own PR, never bundled)
|
||||
PR12 WU11 Alarm ringing restyle (safety-critical review)
|
||||
PR13 WU13 Ecualizador settings screen (5-band restyle)
|
||||
PR14 WU14 Reproductor + per-station EQ sheet
|
||||
PR15 WU15 Recordings library
|
||||
PR16 WU16 Connectivity states
|
||||
PR17 WU17 Welcome / onboarding
|
||||
PR18 WU18 i18n batch, 13 locales
|
||||
```
|
||||
|
||||
18 PRs (was 19 — the EQ spike PR is gone, resolved before planning closed).
|
||||
PR2 is effort-light with no dependencies and may be pulled forward at any point.
|
||||
PR13 **must** merge before PR14 so the per-station EQ sheet reuses the same editor widget.
|
||||
|
||||
Commits: conventional commits only. **No `Co-Authored-By`, no AI attribution, ever.**
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
- The repo has **zero git tags**; the rollback convention is the version-bump commit.
|
||||
- Full rollback: reset `feat/rediseno-funcional` to `17f8e69` on `main` (the `1.1.16+121` state before `1.2.0+122`).
|
||||
- Partial rollback: because delivery is a feature-branch chain, any single PR can be reverted on the tracker branch
|
||||
without touching `main`. Nothing reaches `main` until the tracker merges.
|
||||
- WU2 (Android Auto) produces no code and needs no rollback.
|
||||
- `lib/servicios/navegacion_auto.dart` is untouched, so Android Auto behaviour cannot regress.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- ~~WU12 native EQ spike~~ — **resolved before planning closed**; no longer a dependency. See Engram `sdd/rediseno-funcional/eq-band-spike` (id 2498).
|
||||
- **Radio Browser `/json/countries`** (WU7) — **verified** against the official docs; contract recorded above and in Engram id 2500. No longer a blocker.
|
||||
- **Radio Browser search sort** (WU6) — **not yet verified.** Must be checked against the official docs before it is specced. Per the standing rule, do not assume.
|
||||
- **Translation capacity** for 13 locales (WU18) — an ops dependency, not an engineering one.
|
||||
- No new packages anticipated. `in_app_purchase` stays commented out.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All 5 tabs present and named Escuchar / Buscar / Favoritos / Alarmas / Ajustes, using existing `PluriIconGlyph.*` icons, with `PluriIconGlyph.home` on Escuchar.
|
||||
- [ ] Favoritos renders its tab bar (the documented exemption from the push-chrome rule).
|
||||
- [ ] Every second-level screen renders a 56px header with a back affordance and **no** tab bar.
|
||||
- [ ] `pantalla_ajustes.dart` root renders **4 grouped nav lists**, zero inline controls, and is under 400 lines.
|
||||
- [ ] Escuchar renders an embedded player hero and a "Tus emisoras" grid sourced from favorites; "Ver todas" switches to the Favoritos tab without pushing a route.
|
||||
- [ ] Discovery content (near-you / genres / trending / countries) is reachable from the Buscar tab's landing state.
|
||||
- [ ] No string in the shipped build contains "PRO", a trial duration, or a price. Grep-checkable across `lib/l10n/app_*.arb`.
|
||||
- [ ] The alarm-ringing status label renders without a seconds counter, and the force-stop retry banner is still present.
|
||||
- [ ] `test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart` passes **unmodified**.
|
||||
- [ ] `test/servicios/navegacion_auto_test.dart` passes with `lib/servicios/navegacion_auto.dart` unchanged (`git diff` is empty for that file).
|
||||
- [ ] Alarm edit, skip-next, and delete are all still reachable (tap / hero pill / swipe respectively) — no capability lost.
|
||||
- [ ] Sleep timer and backup/restore are still reachable from Settings.
|
||||
- [ ] `PluriWaveTokens` exposes `listSurface`, `liveGreen`, and `offlineAccent`; no screen file introduces a new raw `Color(0x...)` literal.
|
||||
- [ ] All 13 ARB files have identical key sets, with zero missing keys. `navHome` still exists as a key in all 13 — the rename changed values only.
|
||||
- [ ] **No ARB file maps `navHome` to a "home" concept.** `app_en.arb` reads `"Listen"`, `app_es.arb` reads `"Escuchar"`, and `git diff` shows **13** changed `navHome` lines. A grep for the literals `"Inicio"`/`"Home"` is **not** sufficient — 11 locales express "home" in their own language (`Beranda`, `Accueil`, `Start`, `Início`, `ホーム`, `首页`, `Главная`, `الرئيسية`, `হোম`, `मुखपृष्ठ`, `Inizio`), so the check must be the 13-line diff, not a literal grep.
|
||||
- [ ] `flutter test` green, `flutter analyze` clean, `dart format .` produces no diff.
|
||||
- [ ] The equalizer renders exactly **5** sliders. `assert(bandas.length == 5)` in `preset_ecualizador.dart` is unchanged, and `git diff` is empty for `lib/servicios/servicio_ecualizador.dart` and the band-application block at `lib/servicios/servicio_audio.dart:749-762`.
|
||||
- [ ] `test/servicios/servicio_ecualizador_test.dart`, `test/estado/estado_ecualizador_test.dart`, and `test/servicios/servicio_audio_eq_reapply_test.dart` pass **unmodified**.
|
||||
- [ ] The Países browser calls **`/json/countries`**. Grep confirms `countrycodes` appears nowhere in `lib/`.
|
||||
- [ ] `stationcount` is parsed with `int.tryParse`; a test fixture supplies it as a **string** and the screen renders the count without throwing.
|
||||
- [ ] `lastcheckok` is **not** present in the countries request. All 8 existing station calls still send it.
|
||||
- [ ] Regression tests for the 8 existing `ServicioRadio` station calls exist and pass both before and after the `_get` transport extraction.
|
||||
- [ ] Every external-API claim in the spec, design, and tasks artifacts cites either the official Radio Browser docs or a recorded Engram contract. No uncited API assertions.
|
||||
Reference in New Issue
Block a user