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.
104 lines
4.5 KiB
Dart
104 lines
4.5 KiB
Dart
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);
|