mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
data class QueueItem(
|
data class QueueItem(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -98,6 +99,7 @@ class PlaybackController(
|
|||||||
private var player: PlayerHandle? = null
|
private var player: PlayerHandle? = null
|
||||||
private var connecting = false
|
private var connecting = false
|
||||||
private var pendingPlay: Pair<List<QueueEntry>, Long>? = null
|
private var pendingPlay: Pair<List<QueueEntry>, Long>? = null
|
||||||
|
private var pendingShuffle: QueueEntry? = null
|
||||||
private var positionJob: Job? = null
|
private var positionJob: Job? = null
|
||||||
private var windowJob: Job? = null
|
private var windowJob: Job? = null
|
||||||
private var bufferingJob: Job? = null
|
private var bufferingJob: Job? = null
|
||||||
@@ -139,7 +141,13 @@ class PlaybackController(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
val pending = pendingPlay
|
val pending = pendingPlay
|
||||||
pendingPlay = null
|
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 = {
|
onDisconnected = {
|
||||||
@@ -167,6 +175,63 @@ class PlaybackController(
|
|||||||
startPosition: Long,
|
startPosition: Long,
|
||||||
) = play(listOf(queueRepository.songsEntry(sourceId)), startPosition)
|
) = 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) {
|
fun playAt(position: Long) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
startLock.withLock {
|
startLock.withLock {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ fun AlbumDetailRoute(
|
|||||||
artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS),
|
artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS),
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onSongClick = viewModel::play,
|
onSongClick = viewModel::play,
|
||||||
|
onShuffle = viewModel::shuffle,
|
||||||
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId },
|
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())
|
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),
|
artwork = rememberArtworkColors(viewModel.coverArt(playlist?.coverArt, true), THEME_TRANSITION_MS),
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onSongClick = viewModel::play,
|
onSongClick = viewModel::play,
|
||||||
|
onShuffle = viewModel::shuffle,
|
||||||
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId },
|
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -53,6 +54,7 @@ fun PlaylistDetailScreen(
|
|||||||
artwork: ArtworkColors?,
|
artwork: ArtworkColors?,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSongClick: (Int) -> Unit,
|
onSongClick: (Int) -> Unit,
|
||||||
|
onShuffle: () -> Unit = {},
|
||||||
playingSongId: String? = null,
|
playingSongId: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -69,7 +71,7 @@ fun PlaylistDetailScreen(
|
|||||||
comment = playlist?.comment,
|
comment = playlist?.comment,
|
||||||
hasSongs = songs.itemCount > 0,
|
hasSongs = songs.itemCount > 0,
|
||||||
onPlay = { onSongClick(0) },
|
onPlay = { onSongClick(0) },
|
||||||
onShuffle = {},
|
onShuffle = onShuffle,
|
||||||
onDownload = {},
|
onDownload = {},
|
||||||
onMore = {},
|
onMore = {},
|
||||||
topInset = topInset,
|
topInset = topInset,
|
||||||
|
|||||||
@@ -43,4 +43,10 @@ class PlaylistDetailViewModel(
|
|||||||
playbackController.playPlaylist(sourceId.first(), playlistId, startIndex.toLong())
|
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 }
|
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(
|
private fun seedAlbum(
|
||||||
count: Int,
|
count: Int,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
|
|||||||
Reference in New Issue
Block a user