feat(reproductor): restructure full player with tool-tray and EQ sheet

Restructure pantalla_reproductor.dart onto PluriPushScaffold (design
ADR-2 - this screen is the documented single consumer of titleOverride,
a centered live/not-playing status pill, and the non-default
keyboard_arrow_down leadingIcon). Square art replaces the old circular
hero, favorite moves from the AppBar into the transport row (the
redundant live-indicator dot is dropped - the AppBar pill already covers
that signal), the old separate info chips collapse into a single
subtitle line, and a new quality row surfaces codec/bitrate with a
"Cambiar" action that reconnects the current stream (this app has no
per-station alternate-quality capability to invoke, so this reuses the
same reproducir() call the existing error-state retry button already
uses, rather than a dead button or an invented picker).

The always-expanded recording panel and the standalone sleep-timer
button both become tool-tray tiles (EQ propio / Grabar / sleep timer /
Compartir), each opening its own bottom sheet. "EQ propio" opens a sheet
hosting EcualizadorWidget - the exact same component WU13 restyled for
Settings, bound via the existing presetParaEmisora/guardarPresetPorEmisora
per-station persistence path. No second editor was created; the
multi-device-eq resolution hierarchy is untouched.

pantalla_reproductor.dart had zero test coverage before this commit (907
lines) - writing it first surfaced two pre-existing bugs blocking any
coverage at all, both fixed: initState called estado.reproducir()
directly, which notifies listeners synchronously before its first await
and threw "setState() during build" the instant the screen mounted
against a fresh Provider tree (fixed via addPostFrameCallback); and the
body Column had no scrollable ancestor and overflowed even a generously
tall viewport (fixed by wrapping it in a SingleChildScrollView, a real
UX improvement and not just a test workaround).

The three protected EQ test files (servicio_ecualizador_test.dart,
estado_ecualizador_test.dart, servicio_audio_eq_reapply_test.dart) stay
unmodified. Full suite: 730/730 green (2 skipped, unchanged), up from 713.

size:exception - realized 1,410 changed lines (25 files including this
docs update) against the 450-600 forecast: the restructured screen file
alone is 658 lines (a near-total rewrite of a 907-line file, not a
patch), its new test file (first-ever coverage) is 519 lines, and a new
test fake plus a togglePlay() override account for the rest. Not
splittable: the restructure, the tool tray, and the EQ-sheet wiring are
one cohesive change to one screen.
This commit is contained in:
2026-07-29 14:20:27 +02:00
parent c9fe0ad651
commit dc21732027
20 changed files with 1178 additions and 242 deletions
+64
View File
@@ -77,6 +77,21 @@ class FakeServicioAudio extends ServicioAudio {
emitirEstado(EstadoReproduccion.pausado);
}
// WU14: the real ServicioAudio.togglePlay() reads `_handler.playbackState`
// (a real just_audio-backed handler that requires registrarHandler(), same
// gap already documented for androidAudioSessionIdStream above) — unsafe
// against a bare FakeServicioAudio. Overridden here using only this Fake's
// own state machinery so `pantalla_reproductor.dart`'s play/pause control
// (previously untested) can be exercised safely.
@override
Future<void> togglePlay() async {
if (_estadoActual == EstadoReproduccion.reproduciendo) {
await pausar();
} else {
emitirEstado(EstadoReproduccion.reproduciendo);
}
}
@override
Future<void> setVolumen(double vol) async {
volumenesAplicados.add(vol);
@@ -639,6 +654,55 @@ class FakeServicioGrabacionRadio extends ServicioGrabacionRadio {
Future<void> dispose() => _controller.close();
}
/// WU14: a recording fake that actually responds to `iniciar`/`detener`
/// in-memory, never touching real files or platform channels (`iniciar` on
/// the real `ServicioGrabacionRadio` opens an HTTP stream to the station's
/// URL and writes to disk — unsafe inside a widget test). Records every
/// call for assertions.
class FakeServicioGrabacionRadioActivable extends ServicioGrabacionRadio {
final _controller = StreamController<EstadoGrabacionRadio>.broadcast();
EstadoGrabacionRadio _estadoActual = const EstadoGrabacionRadio.inactiva();
final List<Duration?> duracionesIniciadas = [];
Emisora? ultimaEmisoraIniciada;
int detenerCalls = 0;
@override
EstadoGrabacionRadio get estado => _estadoActual;
@override
Stream<EstadoGrabacionRadio> get estadoStream => _controller.stream;
@override
Future<void> inicializar() async {}
@override
Future<void> iniciar(
Emisora emisora, {
Duration? duracion,
String? directorio,
}) async {
ultimaEmisoraIniciada = emisora;
duracionesIniciadas.add(duracion);
_estadoActual = EstadoGrabacionRadio(
tipo: EstadoGrabacionRadioTipo.grabando,
emisora: emisora,
inicio: DateTime.now(),
duracionObjetivo: duracion,
);
_controller.add(_estadoActual);
}
@override
Future<void> detener() async {
detenerCalls++;
_estadoActual = const EstadoGrabacionRadio.inactiva();
_controller.add(_estadoActual);
}
@override
Future<void> dispose() => _controller.close();
}
Emisora emisoraDemo({
required String uuid,
required String nombre,