feat(escuchar): replace discovery browser with embedded player and favorites grid
Restructures PantallaInicio's top of screen: a new _EscucharHero (square art, live/offline pill, VisualizadorAudio at barras: 30 / altura: 26 / color: liveGreen, a 5-action transport row - favorite, EQ toggle, stop, play/pause, sleep - plus a tool-tray entry chip opening the full player) replaces the old PluriScreenHeader hero, and a new "Tus emisoras" section (favorites preview, capped, "Ver todas") follows it. Per design ADR-7, EstadoRadio stays the single source of truth: the hero is a StatelessWidget with no cached fields, reading emisoraActual via context.select (uuid-based equality scopes rebuilds to real station changes) and the fast-changing playback status via StreamBuilder, the same pattern _Controles/MiniReproductor already use. The still-present discovery sections (_seccionCercanas onward, including the old grid) are deliberately left in place - WU6 relocates them to Buscar and deletes them from here; removing them now would leave that content nowhere until WU6 lands. MiniReproductor gains a `visible` parameter (default true) and a measured `static const double altura`. app.dart passes `visible: indice != RaizPluriWave.escuchar.index`, hiding it visually only (SizedBox.shrink()) while Escuchar is active, since the hero already shows the same station - the State stays mounted so its didChangeDependencies side effect (configurarLocalizaciones, S3-R3) keeps running regardless of tab. altura was measured empirically (72.0, via tester.getSize) rather than guessed, backing a new derived PluriLayout.escucharBottomChromeInset constant now wired into PantallaInicio's own bottom padding. "Ver todas" switches roots via EstadoNavegacionRaiz.irA(favoritos), verified via a NavigatorObserver asserting the push count is unchanged (switches tabs, does not push). Fixed a pre-existing test-infrastructure gap while writing the anti-cache test: no test in this codebase had ever exercised ServicioAudio.androidAudioSessionIdStream against a bare FakeServicioAudio (pantalla_reproductor.dart has always read it but has no test file at all) - the real getter needs registrarHandler() (main.dart, production only) and threw otherwise. Added an empty stream override to FakeServicioAudio, matching VisualizadorAudio's own documented no-native-session fallback. Tests: 614 -> 618 (2 skipped, unchanged). flutter analyze unchanged at 1 pre-existing info. git diff empty for visualizador_audio.dart and estado_radio.dart - this WU touches neither.
This commit is contained in:
@@ -345,37 +345,85 @@ scenario change — see 4.4's note).
|
||||
**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 --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||||
**Modified tests**: `pantalla_inicio_test.dart`, `pantalla_inicio_rebuild_test.dart`, `mini_reproductor_configurar_test.dart`
|
||||
**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`).
|
||||
|
||||
- [ ] 5.1 RED — write the ADR-7 anti-cache test: mutate `EstadoRadio` **from outside the widget tree** (simulating
|
||||
Android Auto / notification-driven playback change) and assert the Escuchar hero follows, with zero cached
|
||||
fields in `State`.
|
||||
- [ ] 5.2 RED — **hazard test**: assert `MiniReproductor` is `visible: false` (renders `SizedBox.shrink()`) while on
|
||||
Escuchar, AND that `configurarLocalizaciones` still ran in `didChangeDependencies`
|
||||
(`mini_reproductor.dart:27-38`, the S3-R3 contract). Removing the widget from the tree would silently break
|
||||
this — the hazard is hiding it structurally, not visually.
|
||||
- [ ] 5.3 RED — assert "Ver todas" calls `context.read<EstadoNavegacionRaiz>().irA(RaizPluriWave.favoritos)` and that
|
||||
`Navigator` depth is unchanged (switches tab, does not push). This only *consumes* WU1's provider.
|
||||
- [ ] 5.4 RED — update `pantalla_inicio_test.dart` / `pantalla_inicio_rebuild_test.dart` for the new content model
|
||||
(hero + "Tus emisoras" grid replaces the discovery grid); confirm `context.select` scoping keeps rebuild count
|
||||
low (existing `MemoLista` pattern).
|
||||
- [ ] 5.5 GREEN — build the Escuchar hero as a `StatelessWidget` reusing `_WaveHero` / `VisualizadorAudio` patterns
|
||||
from `pantalla_reproductor.dart` (square art, `barras: 30`, `altura: 26`, `color: liveGreen`), transport row
|
||||
with sleep as the 5th action, tool-tray entry chip; reads via `context.select<EstadoRadio, T>` per scalar.
|
||||
- [ ] 5.6 GREEN — add `MiniReproductor.altura` as a value **measured from its actual laid-out height at apply time**
|
||||
(e.g. via a `GlobalKey`/`RenderBox` read at build), never a guessed constant; add its `visible` parameter.
|
||||
`_PaginaPrincipal` sets `visible: false` on Escuchar only, keeping `State` mounted.
|
||||
- [ ] 5.7 GREEN — add the derived `PluriLayout` constant (`bottomChromeInset - MiniReproductor.altura`,
|
||||
`pluri_layout.dart:10` currently hardcodes `146` assuming the mini player is present) for Escuchar's content
|
||||
padding.
|
||||
- [ ] 5.8 GREEN — swap the "Tus emisoras" grid data source to `listaFavoritos` (capped) with "Ver todas" wired to
|
||||
`EstadoNavegacionRaiz.irA(RaizPluriWave.favoritos)`.
|
||||
- [ ] 5.9 REFACTOR — leave the discovery-section widgets (`_seccionCercanas`/`_chipGeneros`/etc.) in place for now;
|
||||
WU6 relocates and then deletes them — do not delete here to avoid an intermediate commit with the content
|
||||
nowhere. Note this handoff explicitly in the commit body.
|
||||
- [ ] 5.10 Verify — anti-cache test green; mini-player-hidden-but-side-effect-ran test green; tab-switch-not-push
|
||||
test green; `visualizador_audio.dart` and `estado_radio.dart` show empty `git diff`.
|
||||
- [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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user