diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt index 80a2c45f..b2dedf26 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackRepository.kt @@ -19,6 +19,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration +/** + * Wrapper for [ItemPlaybackDao] to save [ItemPlayback] objects and also convert them into [ChosenStreams] + */ @Singleton class ItemPlaybackRepository @Inject @@ -28,6 +31,13 @@ class ItemPlaybackRepository private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao, private val streamChoiceService: StreamChoiceService, ) { + /** + * Get the chosen source & stream for the given item + * + * This will be either the explicit choice from the user ([ItemPlayback]) or the default choices + * + * @see getChosenItemFromPlayback + */ suspend fun getSelectedTracks( itemId: UUID, item: BaseItem, @@ -40,6 +50,11 @@ class ItemPlaybackRepository return getChosenItemFromPlayback(item, itemPlayback, plc, prefs) } + /** + * Get the chosen source & stream for the given item + * + * This will be either the explicit choice from the provided [ItemPlayback] & [PlaybackLanguageChoice] or the default choices if they are null + */ fun getChosenItemFromPlayback( item: BaseItem, itemPlayback: ItemPlayback?, @@ -86,6 +101,9 @@ class ItemPlaybackRepository } } + /** + * Save the choice for a specific media source for an item + */ suspend fun savePlayVersion( itemId: UUID, sourceId: UUID, @@ -103,6 +121,11 @@ class ItemPlaybackRepository } } + /** + * Save the chosen track (index & type) for an item + * + * Additionally, if the item is part of a TV Series, store the language as the preferred one for this series + */ suspend fun saveTrackSelection( item: BaseItem, itemPlayback: ItemPlayback?, @@ -180,6 +203,9 @@ class ItemPlaybackRepository return toSave.copy(rowId = id) } + /** + * Wrapper for [ItemPlaybackDao.getTrackModifications] to get the [ItemTrackModification] for an item, if any + */ suspend fun getTrackModifications( itemId: UUID, trackIndex: Int, @@ -188,6 +214,9 @@ class ItemPlaybackRepository itemPlaybackDao.getTrackModifications(userId, itemId, trackIndex) } + /** + * Create and save a [ItemTrackModification] via [ItemPlaybackDao.saveItem] + */ suspend fun saveTrackModifications( itemId: UUID, trackIndex: Int, @@ -206,6 +235,9 @@ class ItemPlaybackRepository } } + /** + * Clear the user's source/stream choices for an item + */ suspend fun deleteChosenStreams(chosenStreams: ChosenStreams?) { Timber.d("deleteChosenStreams: %s", chosenStreams) chosenStreams?.plc?.let { @@ -219,6 +251,9 @@ class ItemPlaybackRepository } } +/** + * Info about which media source and streams the user/app has chosen for playback + */ data class ChosenStreams( val itemPlayback: ItemPlayback?, val plc: PlaybackLanguageChoice?, 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 c41c27f0..4e86ae21 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 @@ -274,6 +274,7 @@ fun ContextMenu( onDismissRequest = { chooseVersion = null }, dismissOnClick = true, waitToLoad = params.fromLongClick, + elevation = 3.dp, ) } } @@ -285,6 +286,7 @@ fun ContextMenu( actions.onDeleteItem.invoke(item) onDismissRequest.invoke() }, + elevation = 3.dp, ) } if (showPlayWithDialog) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index 6bd3dfaa..2224a010 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -64,6 +64,7 @@ import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.ui.FontAwesome +import com.github.damontecres.wholphin.ui.formatBitrate import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.playback.SimpleMediaStream @@ -630,7 +631,14 @@ fun chooseVersionParams( SelectedLeadingContent(uuid != null && uuid == chosenSourceId) }, supportingContent = { - videoStream?.displayTitle?.let { Text(text = it) } + val text = + remember { + buildList { + videoStream?.displayTitle?.let(::add) + source.bitrate?.let(::formatBitrate)?.let(::add) + }.joinToString(", ") + } + Text(text) }, onClick = { onClick.invoke(index) }, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt index a0c427b9..eea3c9f9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -56,6 +57,7 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.PreviewTvSpec +import com.github.damontecres.wholphin.ui.data.ChooseVersionParams import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource @@ -63,6 +65,7 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -83,9 +86,11 @@ fun ExpandablePlayButtons( moreOnClick: () -> Unit, trailerOnClick: (Trailer) -> Unit, onConfirmDelete: () -> Unit, + chooseVersionParams: ChooseVersionParams?, buttonOnFocusChanged: (FocusState) -> Unit, modifier: Modifier = Modifier, ) { + val resources = LocalResources.current val firstFocus = remember { FocusRequester() } val restartInteractionSource = remember { MutableInteractionSource() } val restartButtonFocused by restartInteractionSource.collectIsFocusedAsState() @@ -95,6 +100,9 @@ fun ExpandablePlayButtons( firstFocus.tryRequestFocus() } } + + var chooseVersion by remember { mutableStateOf(null) } + LazyRow( horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(8.dp), @@ -129,6 +137,30 @@ fun ExpandablePlayButtons( } } + if (chooseVersionParams != null && chooseVersionParams.mediaSources.size > 1) { + item { + ExpandableFaButton( + title = R.string.version, + iconStringRes = R.string.fa_file_video, + onClick = { + chooseVersion = + chooseVersionParams( + resources, + chooseVersionParams.mediaSources.orEmpty(), + chooseVersionParams.chosenStreams + ?.source + ?.id + ?.toUUIDOrNull(), + ) { idx -> + val source = chooseVersionParams.mediaSources[idx] + chooseVersionParams.onChooseVersion.invoke(source) + } + }, + modifier = Modifier.onFocusChanged(buttonOnFocusChanged), + ) + } + } + // Watched button item("watched") { ExpandableFaButton( @@ -182,6 +214,16 @@ fun ExpandablePlayButtons( ) } } + chooseVersion?.let { params -> + DialogPopup( + showDialog = true, + title = params.title, + dialogItems = params.items, + onDismissRequest = { chooseVersion = null }, + dismissOnClick = true, + waitToLoad = params.fromLongClick, + ) + } } val MinButtonSize = 40.dp @@ -451,6 +493,7 @@ private fun ExpandablePlayButtonsPreview() { trailerOnClick = {}, canDelete = true, onConfirmDelete = {}, + chooseVersionParams = null, modifier = Modifier, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ChooseVersionParams.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ChooseVersionParams.kt new file mode 100644 index 00000000..397a2126 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ChooseVersionParams.kt @@ -0,0 +1,13 @@ +package com.github.damontecres.wholphin.ui.data + +import com.github.damontecres.wholphin.data.ChosenStreams +import org.jellyfin.sdk.model.api.MediaSourceInfo + +/** + * Necessary details & action for choosing a version of some content + */ +data class ChooseVersionParams( + val chosenStreams: ChosenStreams?, + val mediaSources: List, + val onChooseVersion: (MediaSourceInfo) -> Unit, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index fa45058d..dd91e00b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -39,6 +39,7 @@ import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel +import com.github.damontecres.wholphin.ui.data.ChooseVersionParams import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.detail.PlaylistDialog @@ -47,6 +48,7 @@ import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler import kotlinx.coroutines.launch +import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.serializer.toUUID @@ -169,6 +171,7 @@ fun EpisodeDetails( }, canDelete = canDelete, onConfirmDelete = { viewModel.deleteItem(ep) }, + onChooseVersion = { contextActions.onChooseVersion.invoke(ep, it) }, modifier = modifier, ) } @@ -224,6 +227,7 @@ fun EpisodeDetailsContent( moreOnClick: () -> Unit, canDelete: Boolean, onConfirmDelete: () -> Unit, + onChooseVersion: (MediaSourceInfo) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -284,6 +288,14 @@ fun EpisodeDetailsContent( trailerOnClick = {}, canDelete = canDelete, onConfirmDelete = onConfirmDelete, + chooseVersionParams = + remember(chosenStreams, ep, onChooseVersion) { + ChooseVersionParams( + chosenStreams = chosenStreams, + mediaSources = ep.data.mediaSources.orEmpty(), + onChooseVersion = onChooseVersion, + ) + }, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 785ca74e..42742c85 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -54,6 +54,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.PersonContextActions import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel +import com.github.damontecres.wholphin.ui.data.ChooseVersionParams import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.detail.PlaylistDialog @@ -68,6 +69,7 @@ import com.github.damontecres.wholphin.util.DiscoverRequestType import com.github.damontecres.wholphin.util.ExceptionHandler import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.serializer.toUUID @@ -236,6 +238,7 @@ fun MovieDetails( }, canDelete = state.canDelete, onConfirmDelete = { state.movie?.let { viewModel.deleteItem(it) } }, + onChooseVersion = { contextActions.onChooseVersion.invoke(movie, it) }, modifier = modifier, ) } @@ -304,6 +307,7 @@ fun MovieDetailsContent( onClickDiscover: (Int, DiscoverItem) -> Unit, canDelete: Boolean, onConfirmDelete: () -> Unit, + onChooseVersion: (MediaSourceInfo) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -369,6 +373,14 @@ fun MovieDetailsContent( }, canDelete = canDelete, onConfirmDelete = onConfirmDelete, + chooseVersionParams = + remember(state.chosenStreams, movie, onChooseVersion) { + ChooseVersionParams( + chosenStreams = state.chosenStreams, + mediaSources = movie.data.mediaSources.orEmpty(), + onChooseVersion = onChooseVersion, + ) + }, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt index 83987524..600dee28 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt @@ -13,6 +13,8 @@ import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons +import com.github.damontecres.wholphin.ui.data.ChooseVersionParams +import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration @@ -27,6 +29,7 @@ fun FocusedEpisodeFooter( favoriteOnClick: () -> Unit, canDelete: Boolean, onConfirmDelete: () -> Unit, + onChooseVersion: (MediaSourceInfo) -> Unit, modifier: Modifier = Modifier, buttonOnFocusChanged: (FocusState) -> Unit = {}, ) { @@ -52,6 +55,14 @@ fun FocusedEpisodeFooter( trailerOnClick = {}, canDelete = canDelete, onConfirmDelete = onConfirmDelete, + chooseVersionParams = + remember(chosenStreams, ep, onChooseVersion) { + ChooseVersionParams( + chosenStreams = chosenStreams, + mediaSources = ep.data.mediaSources.orEmpty(), + onChooseVersion = onChooseVersion, + ) + }, modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 7532be7a..fed2d342 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -311,6 +311,7 @@ fun SeriesOverview( }, canDelete = { viewModel.canDelete(it, preferences.appPreferences) }, onConfirmDelete = viewModel::deleteItem, + onChooseVersion = contextActions.onChooseVersion, modifier = modifier, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 999ed7c4..5d071387 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -66,6 +66,7 @@ import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.util.rememberDelayedNestedScroll import kotlinx.coroutines.launch +import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.PersonKind import kotlin.time.Duration @@ -97,6 +98,7 @@ fun SeriesOverviewContent( canDelete: (BaseItem) -> Boolean, onConfirmDelete: (BaseItem) -> Unit, onClickExtra: (Int, ExtrasItem) -> Unit, + onChooseVersion: (BaseItem, MediaSourceInfo) -> Unit, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() @@ -323,6 +325,7 @@ fun SeriesOverviewContent( }, canDelete = canDelete.invoke(ep), onConfirmDelete = { onConfirmDelete.invoke(ep) }, + onChooseVersion = { onChooseVersion.invoke(ep, it) }, modifier = Modifier .padding(top = 4.dp)