From a93f183f5f88d61a4cece2d3e019aec376aca883 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 13 Oct 2025 16:41:12 -0400 Subject: [PATCH] Handle shuffling playlists --- .../dolphin/data/model/Playlist.kt | 7 ++- .../dolphin/ui/detail/PlaylistDetails.kt | 46 ++++++++++++++++++- .../damontecres/dolphin/ui/nav/Destination.kt | 1 + .../dolphin/ui/playback/PlaybackViewModel.kt | 13 +++++- app/src/main/res/values/fa_strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 65 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/model/Playlist.kt b/app/src/main/java/com/github/damontecres/dolphin/data/model/Playlist.kt index 7735e326..e97a7b88 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/data/model/Playlist.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/data/model/Playlist.kt @@ -64,6 +64,7 @@ class PlaylistCreator suspend fun createFromPlaylistId( playlistId: UUID, startIndex: Int?, + shuffled: Boolean, ): Playlist { val request = GetPlaylistItemsRequest( @@ -73,6 +74,10 @@ class PlaylistCreator limit = Playlist.MAX_SIZE, ) val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items - return Playlist(items.map { BaseItem.from(it, api) }, 0) + var baseItems = items.map { BaseItem.from(it, api) } + if (shuffled) { + baseItems = baseItems.shuffled() + } + return Playlist(baseItems, 0) } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/PlaylistDetails.kt index e04ecfe6..0072248f 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/PlaylistDetails.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged @@ -51,6 +52,7 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import coil3.compose.AsyncImage +import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.data.model.Library import com.github.damontecres.dolphin.ui.DefaultItemFields @@ -59,6 +61,8 @@ import com.github.damontecres.dolphin.ui.components.DialogItem import com.github.damontecres.dolphin.ui.components.DialogParams import com.github.damontecres.dolphin.ui.components.DialogPopup import com.github.damontecres.dolphin.ui.components.ErrorMessage +import com.github.damontecres.dolphin.ui.components.ExpandableFaButton +import com.github.damontecres.dolphin.ui.components.ExpandablePlayButton import com.github.damontecres.dolphin.ui.components.LoadingPage import com.github.damontecres.dolphin.ui.components.OverviewText import com.github.damontecres.dolphin.ui.enableMarquee @@ -81,6 +85,7 @@ import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID import javax.inject.Inject +import kotlin.time.Duration @HiltViewModel class PlaylistViewModel @@ -148,6 +153,16 @@ fun PlaylistDetails( ), ) }, + onClickPlay = { shuffle -> + viewModel.navigationManager.navigateTo( + Destination.Playback( + itemId = it.id, + positionMs = 0L, + startIndex = 0, + shuffle = shuffle, + ), + ) + }, onLongClickIndex = { index, item -> longClickDialog = DialogParams( @@ -200,6 +215,7 @@ fun PlaylistDetailsContent( items: List, onClickIndex: (Int, BaseItem) -> Unit, onLongClickIndex: (Int, BaseItem) -> Unit, + onClickPlay: (shuffle: Boolean) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, ) { @@ -208,6 +224,8 @@ fun PlaylistDetailsContent( val focus = remember { FocusRequester() } val focusedItem = items.getOrNull(focusedIndex) + val playButtonFocusRequester = remember { FocusRequester() } + Box( modifier = modifier, ) { @@ -248,6 +266,7 @@ fun PlaylistDetailsContent( .fillMaxSize(), ) { Row( + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(32.dp), modifier = Modifier @@ -256,6 +275,8 @@ fun PlaylistDetailsContent( ) { PlaylistDetailsHeader( focusedItem = focusedItem, + onClickPlay = onClickPlay, + playButtonFocusRequester = playButtonFocusRequester, modifier = Modifier .padding(start = 16.dp) @@ -283,7 +304,9 @@ fun PlaylistDetailsContent( // .fillMaxWidth(.8f) .weight(1f) .background( - MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), + MaterialTheme.colorScheme + .surfaceColorAtElevation(1.dp) + .copy(alpha = .75f), shape = RoundedCornerShape(16.dp), ).focusRequester(focusRequester) .focusGroup() @@ -315,6 +338,9 @@ fun PlaylistDetailsContent( if (it.isFocused) { focusedIndex = index } + }.focusProperties { + left = playButtonFocusRequester + previous = playButtonFocusRequester }, ) } @@ -328,12 +354,30 @@ fun PlaylistDetailsContent( @Composable fun PlaylistDetailsHeader( focusedItem: BaseItem?, + onClickPlay: (shuffle: Boolean) -> Unit, + playButtonFocusRequester: FocusRequester, modifier: Modifier = Modifier, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.focusRequester(playButtonFocusRequester), + ) { + ExpandablePlayButton( + title = R.string.play, + resume = Duration.ZERO, + icon = Icons.Default.PlayArrow, + onClick = { onClickPlay.invoke(false) }, + ) + ExpandableFaButton( + title = R.string.shuffle, + iconStringRes = R.string.fa_shuffle, + onClick = { onClickPlay.invoke(true) }, + ) + } Text( text = focusedItem?.title ?: "", color = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt index 4c13c09b..71b4e263 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt @@ -68,6 +68,7 @@ sealed class Destination( val positionMs: Long, @Transient val item: BaseItem? = null, val startIndex: Int? = null, + val shuffle: Boolean = false, ) : Destination(true) { override fun toString(): String = "Playback(itemId=$itemId, positionMs=$positionMs)" diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt index 198c10a1..7c2bf923 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt @@ -1,6 +1,7 @@ package com.github.damontecres.dolphin.ui.playback import android.content.Context +import android.widget.Toast import androidx.annotation.OptIn import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData @@ -144,6 +145,7 @@ class PlaybackViewModel playlistCreator.createFromPlaylistId( queriedItem.id, destination.startIndex, + destination.shuffle, ) withContext(Dispatchers.Main) { this@PlaybackViewModel.playlist.value = playlist @@ -154,7 +156,10 @@ class PlaybackViewModel queriedItem } - play(base, destination.positionMs) + val played = play(base, destination.positionMs) + if (!played) { + playUpNextUp() + } if (!isPlaylist && queriedItem.type == BaseItemKind.EPISODE) { val playlist = @@ -174,7 +179,11 @@ class PlaybackViewModel withContext(Dispatchers.IO) { Timber.i("Playing ${base.id}") if (base.type !in supportItemKinds) { - showToast(context, "Unsupported type '${base.type}', skipping...") + showToast( + context, + "Unsupported type '${base.type}', skipping...", + Toast.LENGTH_SHORT, + ) return@withContext false } dto = base diff --git a/app/src/main/res/values/fa_strings.xml b/app/src/main/res/values/fa_strings.xml index 161d8ff6..8b6e56df 100644 --- a/app/src/main/res/values/fa_strings.xml +++ b/app/src/main/res/values/fa_strings.xml @@ -31,4 +31,5 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afad58ef..c7b96a3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,5 +75,6 @@ Updates No update available Clear image cache + Shuffle