diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index db622187..9d48ae89 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -68,6 +68,14 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl /// new queue, leaving the player "locked" to a corrupted state. int _queueGeneration = 0; + /// Tracks the most recent setQueueFromTracks() input so + /// skipToQueueItem can reconstruct the source list. just_audio + /// requires every source to be built before it can be a skip + /// target, but setQueueFromTracks only builds the initial source + /// and fills the rest in the background — so a skip to an index + /// past the fill front needs to rebuild from the stored tracks. + List _lastTracks = const []; + /// Volume stream for UI subscribers. Mirrors the just_audio player's /// volume directly; set via setVolume(double). Stream get volumeStream => _player.volumeStream; @@ -105,6 +113,16 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl // Reset suppress flag in case a prior backward-fill bailed on // gen check before reaching its `finally`. _suppressIndexUpdates = false; + _lastTracks = tracks; + + // Pause the old source immediately so the previous track stops + // audibly the moment the user taps, instead of bleeding through + // until the new source finishes building. setAudioSources below + // will swap the source list cleanly; pause is the simplest way + // to silence the player during the (possibly multi-100ms) build. + if (_player.playing) { + await _player.pause(); + } // Populate the visible queue + current mediaItem immediately so // the player UI reflects the user's tap before any source has @@ -130,6 +148,23 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl unawaited(_fillRemainingSources(tracks, clampedInitial, myGen)); } + /// Switches playback to the [index]th queue item. The full + /// just_audio source list isn't necessarily built yet + /// (_fillRemainingSources runs in the background), so we + /// reconstruct from the stored TrackRef list rather than calling + /// _player.seek(index: ...) on a possibly-missing source. Calling + /// setQueueFromTracks again is the safe path: it bumps the + /// generation, cancels any in-flight fill, rebuilds source[0] as + /// the target, and re-fills around. play() restarts playback so + /// queue taps feel like "jump to this song" rather than "set the + /// pointer and wait for me to press play." + @override + Future skipToQueueItem(int index) async { + if (index < 0 || index >= _lastTracks.length) return; + await setQueueFromTracks(_lastTracks, initialIndex: index); + await play(); + } + /// Background fill of the rest of the just_audio source list after /// the initial source is playing. Forward direction first (most /// common skipNext target). Backward inserts shift the player's