mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
start a shuffled queue from the album and playlist shuffle buttons
This commit is contained in:
@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.random.Random
|
||||
|
||||
data class QueueItem(
|
||||
val id: String,
|
||||
@@ -98,6 +99,7 @@ class PlaybackController(
|
||||
private var player: PlayerHandle? = null
|
||||
private var connecting = false
|
||||
private var pendingPlay: Pair<List<QueueEntry>, Long>? = null
|
||||
private var pendingShuffle: QueueEntry? = null
|
||||
private var positionJob: Job? = null
|
||||
private var windowJob: Job? = null
|
||||
private var bufferingJob: Job? = null
|
||||
@@ -139,7 +141,13 @@ class PlaybackController(
|
||||
scope.launch {
|
||||
val pending = pendingPlay
|
||||
pendingPlay = null
|
||||
if (pending != null) start(pending.first, pending.second) else restore()
|
||||
val shuffle = pendingShuffle
|
||||
pendingShuffle = null
|
||||
when {
|
||||
pending != null -> start(pending.first, pending.second)
|
||||
shuffle != null -> startShuffled(shuffle)
|
||||
else -> restore()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDisconnected = {
|
||||
@@ -167,6 +175,63 @@ class PlaybackController(
|
||||
startPosition: Long,
|
||||
) = play(listOf(queueRepository.songsEntry(sourceId)), startPosition)
|
||||
|
||||
fun shuffleAlbum(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
) = shufflePlay(queueRepository.albumEntry(sourceId, albumId))
|
||||
|
||||
fun shufflePlaylist(
|
||||
sourceId: Long,
|
||||
playlistId: String,
|
||||
) = shufflePlay(queueRepository.playlistEntry(sourceId, playlistId))
|
||||
|
||||
private fun shufflePlay(entry: QueueEntry) {
|
||||
if (player == null) {
|
||||
pendingShuffle = entry
|
||||
connect()
|
||||
return
|
||||
}
|
||||
scope.launch { startShuffled(entry) }
|
||||
}
|
||||
|
||||
private suspend fun startShuffled(entry: QueueEntry) =
|
||||
startLock.withLock {
|
||||
windowJob?.cancel()
|
||||
windowStart = 0
|
||||
windowEnd = -1
|
||||
lastDurationMs = 0
|
||||
lastError = null
|
||||
lastEdit = null
|
||||
endedHandled = false
|
||||
val previous = snapshot
|
||||
val previousEntry = previous?.entries?.firstOrNull()?.entry
|
||||
val same = previousEntry != null && previousEntry.kind == entry.kind && previousEntry.refId == entry.refId
|
||||
val avoid = if (same) currentPosition()?.let { previous.flatPosition(it) } else null
|
||||
queueRepository.replace(listOf(entry))
|
||||
var snap = queueRepository.snapshot()
|
||||
if (snap.size == 0L) {
|
||||
stopLocked()
|
||||
return@withLock
|
||||
}
|
||||
val start = randomIndex(snap.size, avoid)
|
||||
val order = withContext(Dispatchers.Default) { shuffledOrder(snap.size, start) }
|
||||
queueRepository.setShuffle(true, order)
|
||||
shuffleEnabled = true
|
||||
queueRepository.setCursor(0)
|
||||
snap = queueRepository.snapshot()
|
||||
this@PlaybackController.snapshot = snap
|
||||
loadWindow(0, autoplay = true)
|
||||
}
|
||||
|
||||
private fun randomIndex(
|
||||
size: Long,
|
||||
avoid: Long?,
|
||||
): Long {
|
||||
if (size <= 1L) return 0L
|
||||
val index = Random.nextLong(size)
|
||||
return if (avoid != null && index == avoid) (index + 1L) % size else index
|
||||
}
|
||||
|
||||
fun playAt(position: Long) {
|
||||
scope.launch {
|
||||
startLock.withLock {
|
||||
|
||||
@@ -43,6 +43,7 @@ fun AlbumDetailRoute(
|
||||
artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS),
|
||||
onBack = onBack,
|
||||
onSongClick = viewModel::play,
|
||||
onShuffle = viewModel::shuffle,
|
||||
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,4 +38,10 @@ class AlbumDetailViewModel(
|
||||
playbackController.playAlbum(sourceId.first(), albumId, startIndex.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
fun shuffle() {
|
||||
viewModelScope.launch {
|
||||
playbackController.shuffleAlbum(sourceId.first(), albumId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ fun PlaylistDetailRoute(
|
||||
artwork = rememberArtworkColors(viewModel.coverArt(playlist?.coverArt, true), THEME_TRANSITION_MS),
|
||||
onBack = onBack,
|
||||
onSongClick = viewModel::play,
|
||||
onShuffle = viewModel::shuffle,
|
||||
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId },
|
||||
)
|
||||
}
|
||||
@@ -53,6 +54,7 @@ fun PlaylistDetailScreen(
|
||||
artwork: ArtworkColors?,
|
||||
onBack: () -> Unit,
|
||||
onSongClick: (Int) -> Unit,
|
||||
onShuffle: () -> Unit = {},
|
||||
playingSongId: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -69,7 +71,7 @@ fun PlaylistDetailScreen(
|
||||
comment = playlist?.comment,
|
||||
hasSongs = songs.itemCount > 0,
|
||||
onPlay = { onSongClick(0) },
|
||||
onShuffle = {},
|
||||
onShuffle = onShuffle,
|
||||
onDownload = {},
|
||||
onMore = {},
|
||||
topInset = topInset,
|
||||
|
||||
@@ -43,4 +43,10 @@ class PlaylistDetailViewModel(
|
||||
playbackController.playPlaylist(sourceId.first(), playlistId, startIndex.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
fun shuffle() {
|
||||
viewModelScope.launch {
|
||||
playbackController.shufflePlaylist(sourceId.first(), playlistId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,6 +840,40 @@ class PlaybackControllerTest {
|
||||
await { controller.state.value.isPlaying }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shufflePlayStartsShuffledOnARandomTrack() {
|
||||
seedAlbum(5, sourceId = 1)
|
||||
|
||||
controller.shuffleAlbum(1, "al1")
|
||||
await { controller.state.value.item != null && controller.state.value.shuffle }
|
||||
|
||||
assertTrue(controller.state.value.shuffle)
|
||||
assertEquals(0L, controller.state.value.position)
|
||||
assertTrue(handle.items.first().id in listOf("s1", "s2", "s3", "s4", "s5"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shufflingTheSameAlbumAgainPicksADifferentTrack() {
|
||||
seedAlbum(5, sourceId = 1)
|
||||
|
||||
controller.shuffleAlbum(1, "al1")
|
||||
await { controller.state.value.item != null }
|
||||
val first =
|
||||
controller.state.value.item
|
||||
?.id
|
||||
|
||||
controller.shuffleAlbum(1, "al1")
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id != first
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
controller.state.value.item
|
||||
?.id != first,
|
||||
)
|
||||
}
|
||||
|
||||
private fun seedAlbum(
|
||||
count: Int,
|
||||
sourceId: Long,
|
||||
|
||||
Reference in New Issue
Block a user