feat(auto): queue playback and shuffle for local music folders [size:exception]
Adds "Reproducir carpeta" (sequential) and "Reproducir aleatorio" (Fisher-Yates over the name-sorted order) as folder-scoped playable actions, with auto-advance on track completion and skip next/prev. Isolation from live radio is structural, not disciplinary: the public playMediaItem always clears the local queue on any call, and a new private _encolarCambioFuente is the only path that can advance within it. _cambiarFuente, ControladorReconexion, and the reconnect error path are untouched -- confirmed by a byte-for-byte empty diff on all 4 pre-existing radio/reconnect regression suites, independently re-run before and after (21/21 both times). Handler wiring itself is static-review-only (PluriWaveAudioHandler can't be unit-instantiated); the isolation/advance/race-guard decision logic is extracted into cola_local.dart's pure functions, which are fully unit-tested.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import '../modelos/pista_local.dart';
|
||||
|
||||
/// Immutable snapshot of a local-music play queue (Design ADR-1): an ordered
|
||||
/// list of a folder's direct audio children plus the currently-playing
|
||||
/// index. Pure Dart, no side effects — every "move" method returns a NEW
|
||||
/// instance rather than mutating in place, mirroring how
|
||||
/// `ControladorReconexion` was extracted from `PluriWaveAudioHandler`
|
||||
/// (`controlador_reconexion.dart`) so this stays fully unit-testable without
|
||||
/// the handler (which cannot be instantiated in unit tests — see this
|
||||
/// module's sibling test file's doc comment).
|
||||
class ColaLocal {
|
||||
const ColaLocal({required this.pistas, this.indice = 0});
|
||||
|
||||
/// The folder's direct audio children, in play order — already
|
||||
/// name-sorted or Fisher-Yates shuffled by the caller before
|
||||
/// construction (Design ADR-6). This class itself never re-orders them.
|
||||
final List<NodoLocal> pistas;
|
||||
|
||||
/// The currently-playing position within [pistas].
|
||||
final int indice;
|
||||
|
||||
/// Whether [indice] points at a real entry in [pistas] (Design "empty
|
||||
/// list" edge case: an empty queue has no current track).
|
||||
bool get hayActual => indice >= 0 && indice < pistas.length;
|
||||
|
||||
/// The track at [indice]. Only meaningful when [hayActual] is `true` —
|
||||
/// callers must check it first; this getter does not guard the read
|
||||
/// itself.
|
||||
NodoLocal get actual => pistas[indice];
|
||||
|
||||
/// The queue advanced one position (Design ADR-3's advance flow):
|
||||
/// `null` when [indice] is already the last position (or [pistas] is
|
||||
/// empty) — end-of-queue, per Design ADR-4 (no wraparound to track 1).
|
||||
ColaLocal? conSiguiente() {
|
||||
final siguiente = indice + 1;
|
||||
if (siguiente >= pistas.length) return null;
|
||||
return ColaLocal(pistas: pistas, indice: siguiente);
|
||||
}
|
||||
|
||||
/// The queue moved back one position, CLAMPED at 0 (Design ADR-4:
|
||||
/// `skipToPrevious` at the first track restarts it instead of throwing or
|
||||
/// wrapping to the last track).
|
||||
ColaLocal conAnterior() {
|
||||
final anterior = indice > 0 ? indice - 1 : 0;
|
||||
return ColaLocal(pistas: pistas, indice: anterior);
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of [decidirAvanceCola] (Design ADR-3): a pure decision the caller
|
||||
/// (`PluriWaveAudioHandler._manejarFinPista`) acts on with
|
||||
/// static-review-only field mutations — this function itself has no side
|
||||
/// effects.
|
||||
enum DecisionAvanceCola {
|
||||
/// No advance: not a completion event, no active local queue (radio
|
||||
/// isolation), or the re-entry latch is already armed (an advance is
|
||||
/// already resolving its URI).
|
||||
ninguna,
|
||||
|
||||
/// Advance to the next track — the caller marks the re-entry latch and
|
||||
/// resolves/plays `colaLocal.conSiguiente()`.
|
||||
avanzar,
|
||||
|
||||
/// The queue reached its end — the caller deactivates the queue and stops
|
||||
/// playback (Design ADR-4, no wraparound).
|
||||
desactivar,
|
||||
}
|
||||
|
||||
/// Pure queue-advance decision (Design ADR-3), extracted so it is
|
||||
/// unit-testable without instantiating `PluriWaveAudioHandler` — the
|
||||
/// established `ControladorReconexion` precedent (see this file's class
|
||||
/// doc comment). Deliberately takes NO `just_audio` type: the caller maps
|
||||
/// `ProcessingState.completed` to [trackCompletado] before calling this.
|
||||
///
|
||||
/// Double-gated: [trackCompletado] must be `true` AND [colaLocal] must be
|
||||
/// non-null (radio never sets `_colaLocal`, so a `null` queue is a
|
||||
/// structural proxy for "not local-queue playback" — Design "single
|
||||
/// load-bearing invariant"). [avanzandoCola] is the re-entry latch: a
|
||||
/// second `completed` emission while an earlier advance is still resolving
|
||||
/// its URI must be a no-op, not a double-advance.
|
||||
DecisionAvanceCola decidirAvanceCola({
|
||||
required ColaLocal? colaLocal,
|
||||
required bool avanzandoCola,
|
||||
required bool trackCompletado,
|
||||
}) {
|
||||
if (!trackCompletado) return DecisionAvanceCola.ninguna;
|
||||
if (colaLocal == null) return DecisionAvanceCola.ninguna;
|
||||
if (avanzandoCola) return DecisionAvanceCola.ninguna;
|
||||
return colaLocal.conSiguiente() == null
|
||||
? DecisionAvanceCola.desactivar
|
||||
: DecisionAvanceCola.avanzar;
|
||||
}
|
||||
|
||||
/// Mid-await race guard (Design ADR-3's advance flow): `true` only when
|
||||
/// [siguienteEsperado] is the EXACT SAME instance still installed as
|
||||
/// [colaLocalActual] — a deliberate `identical()` check, NOT `==`, so a
|
||||
/// user action that replaced `_colaLocal` with a structurally-equal-but
|
||||
/// distinct `ColaLocal` during the async URI-resolve gap is correctly
|
||||
/// detected as stale and aborts the advance, instead of silently racing an
|
||||
/// external play/stop.
|
||||
bool avanceEsValido(
|
||||
ColaLocal? colaLocalActual,
|
||||
ColaLocal? siguienteEsperado,
|
||||
) => identical(colaLocalActual, siguienteEsperado);
|
||||
Reference in New Issue
Block a user