Add a shortcut to chosing the version of an item (#1522)
## Description Based on feedback from users, many felt it was difficult to 1) tell if multiple version of an item existed and 2) choose the version they want So this PR adds a new expandable button next to Play/Resume/Restart to choose the version. It is a shortcut to the More->Choose Version dialog, but also serves as a more prominent indicator that there are multiple versions. The button is only shown if there are 2+ versions to choose from. Also adds displaying the bitrate in the choose version dialog just in case there are identically named versions with the same resolution. Dev note: I also added some docs for classes & functions related to choosing a version ### Related issues Related to #652 ### Testing Emulator ## Screenshots <img width="1280" height="720" alt="choose_version_button_unfocused Large" src="https://github.com/user-attachments/assets/4f6051ef-b6ae-4fad-9d54-3840523350ed" /> <img width="1280" height="720" alt="choose_version_button Large" src="https://github.com/user-attachments/assets/0e8412bc-2d18-4c6c-987a-2f067363ad2c" /> ## AI or LLM usage None
This commit is contained in:
parent
99fab9a383
commit
d1b8c09615
10 changed files with 141 additions and 1 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<DialogParams?>(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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MediaSourceInfo>,
|
||||
val onChooseVersion: (MediaSourceInfo) -> Unit,
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ fun SeriesOverview(
|
|||
},
|
||||
canDelete = { viewModel.canDelete(it, preferences.appPreferences) },
|
||||
onConfirmDelete = viewModel::deleteItem,
|
||||
onChooseVersion = contextActions.onChooseVersion,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue