Tint the mini player's reconnecting/error sub-states with the
offlineAccent token (added in WU1, unused until now): the status
label, the reconnect spinner, and the error retry icon now read as
visually distinct "connectivity trouble" states instead of blending
into the ordinary loading/paused look. Plain buffering keeps the
default colour, confirmed by a dedicated regression test.
Verify-first gate (task 16.1): ControladorReconexion.intentos exists,
but ServicioAudio never surfaces it past a debug log line, and its
estadoStream only carries the EstadoReproduccion enum. Adding an
attempt-count label would require a getter/stream on
servicio_audio.dart, one of the files this change must keep at an
empty diff against main. Ship the restyle without the counter, per
the risk register's own fallback.
WU16.
Restyle the Ecualizador settings screen to the new visual language while
keeping the equalizer at 5 bands (spike-resolved, Engram id 2498 - band
count is device-reported via just_audio's AndroidEqualizer, not app-chosen;
the approved mockup's 7 sliders would silently no-op on typical hardware).
- Restyle EcualizadorWidget in place: strip its internal title + preset
chip row (the pushed screen's header now carries the title), add a
habilitado parameter that greys/disables every slider when EQ is off.
Widen PresetsEcualizadorWidget additively (personalizados param) so
custom presets can join the chip row without a second implementation.
- Add servicio_presets_personalizados.dart (new file, own SharedPreferences
key eq_custom_presets_v1) for custom EQ preset persistence - kept out of
servicio_ecualizador.dart, which has an empty-git-diff success criterion
for this change. preset_ecualizador.dart is unchanged: a custom preset is
just a PresetEcualizador with a user-supplied name.
- Extend EstadoEcualizador with presetsPersonalizados,
guardarPresetPersonalizado (validates non-empty name),
eliminarPresetPersonalizado. The load is a new explicit
cargarPresetsPersonalizados(), deliberately NOT folded into
cargarPersistido(): that method is exercised ~30 times by
estado_ecualizador_test.dart (protected, must stay unmodified) via Fakes
only, with no SharedPreferences awareness in that file.
- Build out the Ecualizador screen body: base-vs-per-station explainer
banner, a "Salida activa" row surfaced on the main screen (previously
Advanced-only), an "Emisoras con ajuste propio" drill-down sourced from
the existing presetsPorEmisora map, and a "Guardar como preset" action.
New coverage lives in new files rather than touching the three protected
EQ test files: ecualizador_widget_test.dart (component-level, did not
exist before this commit), servicio_presets_personalizados_test.dart, and
estado_ecualizador_presets_personalizados_test.dart. servicio_ecualizador.dart,
servicio_audio.dart and the three protected EQ test files keep an empty
git diff. Full suite: 713/713 green (2 skipped, unchanged), up from 682.
size:exception - realized 1,954 changed lines (25 files, plus this docs
update) against the 400-550 forecast: lib/ + ARB alone is ~650 lines, near
the top of the forecast band by itself since this WU also had to build out
a screen body WU3a only stubbed; the rest is 4 test files (675 lines) and
11 new ARB keys regenerating 13 lib/l10n/gen files (~546 lines) - the same
pattern every prior work unit in this branch has hit. Not splittable: WU14
reuses this unit's editor component by exact runtime type and cannot begin
until this lands as a whole.
Replace the native showTimePicker dialog in the alarm editor sheet with
a giant inline HH:MM editor (drag/tap to adjust, wraps at 23:59-00:00).
Weekday circles now render unconditionally (disabled outside weekly
mode) instead of being gated behind an `if`.
The date field, fallback-station picker, and sound dropdown are not
dropped: per resolution 3 they move into a collapsed "Advanced" section
so the mockup's weekday-circles-only layout does not lose capability.
Volume/fade-in sliders get a cosmetic type-scale restyle only.
size:exception: 993 changed lines (891+/102-) against the 500-650
forecast - lib/ production code alone is 429 lines, within band; new
test files and 13 regenerated l10n/gen files account for the rest, the
same pattern every prior work unit in this branch has hit.
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.
AudioService.init has no internal timeout and an unhandled
onConnectionSuspended case in its MediaBrowser self-bind; under bind
contention with the car's connection it can hang forever, so runApp
never ran (black car screen, white phone UI until process kill).
Race init against an 8s timeout without ever re-calling it: on timeout
run a bootstrap app that waits on the same future, wires the handler
exactly once when it resolves, reports errors via FlutterError, and
swaps to the real app. Auto browse sources now register before the
init await since they take no handler dependency.
- Replace all hardcoded Color literals outside lib/tema with theme tokens (new static brand palette in PluriWaveTokens); media notification uses the brand color instead of the Material default purple
- Favorite button on station cards grows to a 48dp target and becomes an independent semantics node for screen readers (Semantics container fix)
- All flutter_animate call sites route through the PluriAnimate reduced-motion gate (zero direct .animate() left)
- Locale-aware short dates via intl DateFormat (new lib/l10n/formato_fechas.dart) replacing the hardcoded DD/MM/YYYY; proper plural messages for the favorites counter; example stream URL as a localized key - all 13 locales
- Rounded shimmer placeholders matching card radii; shimmer loading state in search instead of a bare spinner; rounded icon variants unified in settings; bottom-sheet conventions on the custom station form
- Fix latent debug crash: vacation editor read AppLocalizations in initState
- 11 new tests (121 total green), flutter analyze clean
- Construct the audio player with an enlarged live-stream buffer (15-50s forward cushion, 2.5s to start, 5s after rebuffer) so short network drops play through silently
- Add reconnect-on-stall state machine with bounded exponential backoff (1/2/4/8/16s, ~90s total window, 5 attempts) that re-prepares to the live edge; backoff/decision logic extracted to controlador_reconexion.dart as pure testable code
- Surface a new reconnecting playback state in the mini player and full player (localized in all 13 locales) instead of error dialogs during the retry window; a single friendly error appears only after exhaustion
- Guard interplay: user pause/stop cancels retries, audio interruptions cancel reconnect, alarm wake-up path keeps precedence, recording fails cleanly during drops
- Reset retry budget on station change; route stream timeouts through the network-error class
- 10 new tests (99 total green), flutter analyze clean
- Integrate audio_session (new servicio_audio_session.dart): incoming calls pause the radio and resume on end, headphone unplug pauses without auto-resume, permanent focus loss never auto-resumes, duck lowers volume
- Add play-intent flag to ServicioAudio so interruption handling and future reconnect logic can distinguish user pause from system-driven stops
- Eliminate read-modify-write race in ServicioAlarmas with an in-memory cache and single-writer queue across all mutations; recalcularTodas persists only when state actually changed
- Convert ServicioAlarmasAndroid static StreamController/handler to injectable instance fields, restoring test isolation
- Inject a single cached SharedPreferences from main.dart across services and state (removes 23 inline getInstance() calls)
- Move configurarLocalizaciones out of MiniReproductor.build() (was running on every rebuild during playback)
- Bound the alarm fire-dedup set (cap 200 entries, 24h pruning)
- 12 new tests (89 total green), flutter analyze clean