Handle shuffling playlists

This commit is contained in:
Damontecres 2025-10-13 16:41:12 -04:00
parent 9148ca0719
commit a93f183f5f
No known key found for this signature in database
6 changed files with 65 additions and 4 deletions

View file

@ -64,6 +64,7 @@ class PlaylistCreator
suspend fun createFromPlaylistId( suspend fun createFromPlaylistId(
playlistId: UUID, playlistId: UUID,
startIndex: Int?, startIndex: Int?,
shuffled: Boolean,
): Playlist { ): Playlist {
val request = val request =
GetPlaylistItemsRequest( GetPlaylistItemsRequest(
@ -73,6 +74,10 @@ class PlaylistCreator
limit = Playlist.MAX_SIZE, limit = Playlist.MAX_SIZE,
) )
val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items 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)
} }
} }

View file

@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
@ -51,6 +52,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage 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.BaseItem
import com.github.damontecres.dolphin.data.model.Library import com.github.damontecres.dolphin.data.model.Library
import com.github.damontecres.dolphin.ui.DefaultItemFields 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.DialogParams
import com.github.damontecres.dolphin.ui.components.DialogPopup import com.github.damontecres.dolphin.ui.components.DialogPopup
import com.github.damontecres.dolphin.ui.components.ErrorMessage 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.LoadingPage
import com.github.damontecres.dolphin.ui.components.OverviewText import com.github.damontecres.dolphin.ui.components.OverviewText
import com.github.damontecres.dolphin.ui.enableMarquee 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 org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration
@HiltViewModel @HiltViewModel
class PlaylistViewModel 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 -> onLongClickIndex = { index, item ->
longClickDialog = longClickDialog =
DialogParams( DialogParams(
@ -200,6 +215,7 @@ fun PlaylistDetailsContent(
items: List<BaseItem?>, items: List<BaseItem?>,
onClickIndex: (Int, BaseItem) -> Unit, onClickIndex: (Int, BaseItem) -> Unit,
onLongClickIndex: (Int, BaseItem) -> Unit, onLongClickIndex: (Int, BaseItem) -> Unit,
onClickPlay: (shuffle: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() }, focusRequester: FocusRequester = remember { FocusRequester() },
) { ) {
@ -208,6 +224,8 @@ fun PlaylistDetailsContent(
val focus = remember { FocusRequester() } val focus = remember { FocusRequester() }
val focusedItem = items.getOrNull(focusedIndex) val focusedItem = items.getOrNull(focusedIndex)
val playButtonFocusRequester = remember { FocusRequester() }
Box( Box(
modifier = modifier, modifier = modifier,
) { ) {
@ -248,6 +266,7 @@ fun PlaylistDetailsContent(
.fillMaxSize(), .fillMaxSize(),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(32.dp), horizontalArrangement = Arrangement.spacedBy(32.dp),
modifier = modifier =
Modifier Modifier
@ -256,6 +275,8 @@ fun PlaylistDetailsContent(
) { ) {
PlaylistDetailsHeader( PlaylistDetailsHeader(
focusedItem = focusedItem, focusedItem = focusedItem,
onClickPlay = onClickPlay,
playButtonFocusRequester = playButtonFocusRequester,
modifier = modifier =
Modifier Modifier
.padding(start = 16.dp) .padding(start = 16.dp)
@ -283,7 +304,9 @@ fun PlaylistDetailsContent(
// .fillMaxWidth(.8f) // .fillMaxWidth(.8f)
.weight(1f) .weight(1f)
.background( .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), MaterialTheme.colorScheme
.surfaceColorAtElevation(1.dp)
.copy(alpha = .75f),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
).focusRequester(focusRequester) ).focusRequester(focusRequester)
.focusGroup() .focusGroup()
@ -315,6 +338,9 @@ fun PlaylistDetailsContent(
if (it.isFocused) { if (it.isFocused) {
focusedIndex = index focusedIndex = index
} }
}.focusProperties {
left = playButtonFocusRequester
previous = playButtonFocusRequester
}, },
) )
} }
@ -328,12 +354,30 @@ fun PlaylistDetailsContent(
@Composable @Composable
fun PlaylistDetailsHeader( fun PlaylistDetailsHeader(
focusedItem: BaseItem?, focusedItem: BaseItem?,
onClickPlay: (shuffle: Boolean) -> Unit,
playButtonFocusRequester: FocusRequester,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier, 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(
text = focusedItem?.title ?: "", text = focusedItem?.title ?: "",
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,

View file

@ -68,6 +68,7 @@ sealed class Destination(
val positionMs: Long, val positionMs: Long,
@Transient val item: BaseItem? = null, @Transient val item: BaseItem? = null,
val startIndex: Int? = null, val startIndex: Int? = null,
val shuffle: Boolean = false,
) : Destination(true) { ) : Destination(true) {
override fun toString(): String = "Playback(itemId=$itemId, positionMs=$positionMs)" override fun toString(): String = "Playback(itemId=$itemId, positionMs=$positionMs)"

View file

@ -1,6 +1,7 @@
package com.github.damontecres.dolphin.ui.playback package com.github.damontecres.dolphin.ui.playback
import android.content.Context import android.content.Context
import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -144,6 +145,7 @@ class PlaybackViewModel
playlistCreator.createFromPlaylistId( playlistCreator.createFromPlaylistId(
queriedItem.id, queriedItem.id,
destination.startIndex, destination.startIndex,
destination.shuffle,
) )
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@PlaybackViewModel.playlist.value = playlist this@PlaybackViewModel.playlist.value = playlist
@ -154,7 +156,10 @@ class PlaybackViewModel
queriedItem queriedItem
} }
play(base, destination.positionMs) val played = play(base, destination.positionMs)
if (!played) {
playUpNextUp()
}
if (!isPlaylist && queriedItem.type == BaseItemKind.EPISODE) { if (!isPlaylist && queriedItem.type == BaseItemKind.EPISODE) {
val playlist = val playlist =
@ -174,7 +179,11 @@ class PlaybackViewModel
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Timber.i("Playing ${base.id}") Timber.i("Playing ${base.id}")
if (base.type !in supportItemKinds) { 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 return@withContext false
} }
dto = base dto = base

View file

@ -31,4 +31,5 @@
<string name="fa_eye" translatable="false">&#xf06e;</string> <string name="fa_eye" translatable="false">&#xf06e;</string>
<string name="fa_eye_slash" translatable="false">&#xf070;</string> <string name="fa_eye_slash" translatable="false">&#xf070;</string>
<string name="fa_arrow_left_arrow_right" translatable="false">&#xf0ec;</string> <string name="fa_arrow_left_arrow_right" translatable="false">&#xf0ec;</string>
<string name="fa_shuffle" translatable="false">&#xf074;</string>
</resources> </resources>

View file

@ -75,5 +75,6 @@
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="no_update_available">No update available</string> <string name="no_update_available">No update available</string>
<string name="clear_image_cache">Clear image cache</string> <string name="clear_image_cache">Clear image cache</string>
<string name="shuffle">Shuffle</string>
</resources> </resources>