diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt index 055c9186..0b2e85e3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -23,6 +23,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.playlistsApi +import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind @@ -202,28 +203,72 @@ class PlaylistCreator BaseItemKind.SEASON -> { val seriesId = item.seriesId if (seriesId != null) { - PlaylistCreationResult.Success( - createFromEpisode( - seriesId = seriesId, - seasonId = item.id, - episodeId = null, - shuffled = shuffled, - ), - ) + if (shuffled) { + api.tvShowsApi + .getEpisodes( + seriesId = seriesId, + seasonId = item.id, + limit = Playlist.MAX_SIZE, + sortBy = ItemSortBy.RANDOM, + fields = DefaultItemFields, + ).content.items + .convertAndAddParts() + .let { + PlaylistCreationResult.Success(Playlist(it)) + } + } else { + PlaylistCreationResult.Success( + createFromEpisode( + seriesId = seriesId, + seasonId = item.id, + episodeId = null, + shuffled = shuffled, + ), + ) + } } else { PlaylistCreationResult.Error(null, "Episode has no seriesId") } } BaseItemKind.SERIES -> { - PlaylistCreationResult.Success( - createFromEpisode( - seriesId = item.id, - seasonId = null, - episodeId = null, - shuffled = shuffled, - ), - ) + if (shuffled) { + api.tvShowsApi + .getEpisodes( + seriesId = item.id, + limit = Playlist.MAX_SIZE, + sortBy = ItemSortBy.RANDOM, + fields = DefaultItemFields, + ).content.items + .convertAndAddParts() + .let { + PlaylistCreationResult.Success(Playlist(it)) + } + } else { + val result by api.tvShowsApi.getNextUp(seriesId = item.id) + val nextUp = + result.items.firstOrNull() ?: api.tvShowsApi + .getEpisodes( + item.id, + limit = 1, + ).content.items + .firstOrNull() + if (nextUp != null) { + PlaylistCreationResult.Success( + createFromEpisode( + seriesId = item.id, + seasonId = null, + episodeId = nextUp.id, + shuffled = shuffled, + ), + ) + } else { + PlaylistCreationResult.Error( + null, + "Could not determine next up episode for series: " + item.id, + ) + } + } } BaseItemKind.PLAYLIST -> { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt index 4e86ae21..3a592bbc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt @@ -28,6 +28,7 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.util.supportedPlayableTypes +import com.github.damontecres.wholphin.util.supportedShufflableTypes import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind @@ -355,7 +356,6 @@ private fun buildContextMenuItems( DialogItem( resources.getString(R.string.resume), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, ) { actions.navigateTo( @@ -385,7 +385,6 @@ private fun buildContextMenuItems( DialogItem( resources.getString(R.string.play), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, ) { actions.navigateTo( @@ -398,6 +397,22 @@ private fun buildContextMenuItems( ) } } + if (item.type in supportedShufflableTypes) { + add( + DialogItem( + resources.getString(R.string.shuffle), + R.string.fa_shuffle, + dismissOnClick = true, + ) { + actions.navigateTo( + Destination.PlaybackList( + itemId = item.id, + shuffle = true, + ), + ) + }, + ) + } if (showStreamChoices) { item.data.mediaSources?.letNotEmpty { sources -> val source = @@ -734,7 +749,6 @@ fun buildContextForMusic( DialogItem( resources.getString(R.string.play), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, ) { actions.onClickPlay(index, item) @@ -744,7 +758,6 @@ fun buildContextForMusic( DialogItem( resources.getString(R.string.play_next), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, ) { actions.onClickPlayNext(index, item) @@ -867,7 +880,6 @@ fun buildContextForMusicQueue( DialogItem( resources.getString(R.string.play), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, ) { actions.onClickPlay(index, item) @@ -877,7 +889,6 @@ fun buildContextForMusicQueue( DialogItem( resources.getString(R.string.play_next), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, ) { actions.onClickPlayNext(index, item) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt deleted file mode 100644 index 73ad4a2e..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt +++ /dev/null @@ -1,193 +0,0 @@ -package com.github.damontecres.wholphin.ui.detail - -import android.content.res.Resources -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.ui.graphics.Color -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.ui.components.DialogItem -import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.util.supportedPlayableTypes -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -data class MoreDialogActions( - val navigateTo: (Destination) -> Unit, - val onClickWatch: (UUID, Boolean) -> Unit, - val onClickFavorite: (UUID, Boolean) -> Unit, - val onClickAddPlaylist: (UUID) -> Unit, - val onSendMediaInfo: (UUID) -> Unit, - val onClickDelete: (BaseItem) -> Unit, - val onClickGoTo: (BaseItem) -> Unit = { navigateTo(it.destination()) }, - val onClickRemoveFromNextUp: (BaseItem) -> Unit = {}, - val onClickAddToQueue: (BaseItem) -> Unit = {}, -) - -fun buildMoreDialogItemsForHome( - resources: Resources, - item: BaseItem, - seriesId: UUID?, - playbackPosition: Duration, - watched: Boolean, - favorite: Boolean, - canDelete: Boolean, - actions: MoreDialogActions, - canRemoveContinueWatching: Boolean = false, - canRemoveNextUp: Boolean = false, -): List = - buildList { - val itemId = item.id - add( - DialogItem( - resources.getString(R.string.go_to), - Icons.Default.ArrowForward, - ) { - actions.onClickGoTo(item) - }, - ) - if (item.type in supportedPlayableTypes) { - if (playbackPosition >= 1.seconds) { - add( - DialogItem( - resources.getString(R.string.resume), - Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), - ) { - actions.navigateTo( - Destination.Playback( - itemId, - playbackPosition.inWholeMilliseconds, - ), - ) - }, - ) - add( - DialogItem( - resources.getString(R.string.restart), - Icons.Default.Refresh, -// iconColor = Color.Green.copy(alpha = .8f), - ) { - actions.navigateTo( - Destination.Playback( - itemId, - 0L, - ), - ) - }, - ) - } else { - add( - DialogItem( - resources.getString(R.string.play), - Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), - ) { - actions.navigateTo( - Destination.Playback( - itemId, - 0L, - ), - ) - }, - ) - } - } - if (item.type == BaseItemKind.MUSIC_ALBUM) { - add( - DialogItem( - resources.getString(R.string.add_to_queue), - Icons.Default.Add, - ) { - actions.onClickAddToQueue(item) - }, - ) - } - add( - DialogItem( - text = R.string.add_to_playlist, - iconStringRes = R.string.fa_list_ul, - ) { - actions.onClickAddPlaylist.invoke(itemId) - }, - ) - if (canDelete) { - add( - DialogItem( - resources.getString(R.string.delete), - Icons.Default.Delete, - iconColor = Color.Red.copy(alpha = .8f), - ) { - actions.onClickDelete.invoke(item) - }, - ) - } - if (canRemoveContinueWatching && !watched && playbackPosition > Duration.ZERO) { - add( - DialogItem( - text = R.string.remove_continue_watching, - iconStringRes = R.string.fa_eye, - ) { - actions.onClickWatch.invoke(itemId, false) - }, - ) - } - if (canRemoveNextUp && item.type == BaseItemKind.EPISODE && item.data.seriesId != null) { - add( - DialogItem( - text = R.string.remove_next_up, - iconStringRes = R.string.fa_tag, - ) { - actions.onClickRemoveFromNextUp.invoke(item) - }, - ) - } - add( - DialogItem( - text = if (watched) R.string.mark_unwatched else R.string.mark_watched, - iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash, - ) { - actions.onClickWatch.invoke(itemId, !watched) - }, - ) - add( - DialogItem( - text = if (favorite) R.string.remove_favorite else R.string.add_favorite, - iconStringRes = R.string.fa_heart, - iconColor = if (favorite) Color.Red else Color.Unspecified, - ) { - actions.onClickFavorite.invoke(itemId, !favorite) - }, - ) - seriesId?.let { - add( - DialogItem( - resources.getString(R.string.go_to_series), - Icons.AutoMirrored.Filled.ArrowForward, - ) { - actions.navigateTo( - Destination.MediaItem( - it, - BaseItemKind.SERIES, - null, - ), - ) - }, - ) - } - add( - DialogItem( - text = R.string.send_media_info_log_to_server, - iconStringRes = R.string.fa_file_video, - ) { - actions.onSendMediaInfo.invoke(itemId) - }, - ) - } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt index f94f7381..df4408fb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -113,7 +112,6 @@ fun PlaylistList( leadingContent = { Icon( imageVector = Icons.Default.Add, - tint = Color.Green.copy(.7f), contentDescription = "Add", ) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index a493cab3..5b87b3cd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource @@ -518,7 +517,6 @@ fun buildDialogForSeason( DialogItem( resources.getString(R.string.play), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), ) { onClickPlay.invoke(false) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt index 53dabb47..04daa260 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt @@ -159,7 +159,6 @@ fun ProgramDialog( Icon( imageVector = Icons.Default.PlayArrow, contentDescription = stringResource(R.string.delete), - tint = Color.Green.copy(alpha = .75f), ) Text( text = stringResource(R.string.watch_live), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt index 55a8c515..e4cc19ec 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt @@ -259,7 +259,6 @@ fun TvGuideGrid( DialogItem( watchLiveStr, Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), dismissOnClick = true, ) { viewModel.navigationManager.navigateTo( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 7c437f21..69639590 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -742,7 +742,6 @@ fun buildDialogForSeason( DialogItem( resources.getString(R.string.play), Icons.Default.PlayArrow, - iconColor = Color.Green.copy(alpha = .8f), ) { onClickPlay.invoke(false) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 696e2eb9..f197998f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -315,16 +315,24 @@ class PlaybackViewModel val playlistItem = if (queriedItem.type.playable) { PlaylistItem.Media(BaseItem(queriedItem, false)) - } else if (destination is Destination.PlaybackList) { + } else { val playlistResult = - playlistCreator.createFrom( - item = queriedItem, - startIndex = destination.startIndex ?: 0, - sortAndDirection = destination.sortAndDirection, - shuffled = destination.shuffle, - recursive = destination.recursive, - filter = destination.filter, - ) + if (destination is Destination.PlaybackList) { + playlistCreator.createFrom( + item = queriedItem, + startIndex = destination.startIndex ?: 0, + sortAndDirection = destination.sortAndDirection, + shuffled = destination.shuffle, + recursive = destination.recursive, + filter = destination.filter, + ) + } else { + // Try to create a playlist + playlistCreator.createFrom( + item = queriedItem, + recursive = true, + ) + } when (val r = playlistResult) { is PlaylistCreationResult.Error -> { _state.update { it.copy(loading = LoadingState.Error(r.message, r.ex)) } @@ -343,8 +351,6 @@ class PlaybackViewModel r.playlist.items.first() } } - } else { - throw IllegalArgumentException("Item is not playable and not PlaybackList: ${queriedItem.type}") } viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt index 0c577571..531a4f9a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt @@ -53,4 +53,16 @@ val supportedPlayableTypes = BaseItemKind.TV_CHANNEL, BaseItemKind.TV_PROGRAM, BaseItemKind.RECORDING, + BaseItemKind.SEASON, + BaseItemKind.SERIES, + BaseItemKind.BOX_SET, + BaseItemKind.PLAYLIST, + ) + +val supportedShufflableTypes = + setOf( + BaseItemKind.SEASON, + BaseItemKind.SERIES, + BaseItemKind.BOX_SET, + BaseItemKind.PLAYLIST, )