Play item directly when clicking play button on remote (#424)

On the home page or any grid, if focused on a "playable" item (movie,
episode, etc), pressing the Play or Play-Pause button on the remote will
immediately start playback.

Dev note: Also guards against an edge case where an item passed into
playback doesn't have media sources due to more limited item fields in
the query.
This commit is contained in:
damontecres 2025-12-11 12:52:13 -05:00 committed by GitHub
parent c97553f82a
commit 319b46983f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 49 additions and 8 deletions

View file

@ -579,7 +579,10 @@ fun CollectionFolderGrid(
defaultViewOptions = defaultViewOptions, defaultViewOptions = defaultViewOptions,
onSaveViewOptions = { viewModel.saveViewOptions(it) }, onSaveViewOptions = { viewModel.saveViewOptions(it) },
playEnabled = playEnabled, playEnabled = playEnabled,
onClickPlay = { shuffle -> onClickPlay = { _, item ->
viewModel.navigationManager.navigateTo(Destination.Playback(item))
},
onClickPlayAll = { shuffle ->
itemId.toUUIDOrNull()?.let { itemId.toUUIDOrNull()?.let {
viewModel.navigationManager.navigateTo( viewModel.navigationManager.navigateTo(
Destination.PlaybackList( Destination.PlaybackList(
@ -678,7 +681,8 @@ fun CollectionFolderGridContent(
letterPosition: suspend (Char) -> Int, letterPosition: suspend (Char) -> Int,
sortOptions: List<ItemSortBy>, sortOptions: List<ItemSortBy>,
playEnabled: Boolean, playEnabled: Boolean,
onClickPlay: (shuffle: Boolean) -> Unit, onClickPlayAll: (shuffle: Boolean) -> Unit,
onClickPlay: (Int, BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showTitle: Boolean = true, showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
@ -786,12 +790,12 @@ fun CollectionFolderGridContent(
title = R.string.play, title = R.string.play,
resume = Duration.ZERO, resume = Duration.ZERO,
icon = Icons.Default.PlayArrow, icon = Icons.Default.PlayArrow,
onClick = { onClickPlay.invoke(false) }, onClick = { onClickPlayAll.invoke(false) },
) )
ExpandableFaButton( ExpandableFaButton(
title = R.string.shuffle, title = R.string.shuffle,
iconStringRes = R.string.fa_shuffle, iconStringRes = R.string.fa_shuffle,
onClick = { onClickPlay.invoke(true) }, onClick = { onClickPlayAll.invoke(true) },
) )
} }
} }
@ -813,6 +817,7 @@ fun CollectionFolderGridContent(
pager = pager, pager = pager,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickPlay = onClickPlay,
letterPosition = letterPosition, letterPosition = letterPosition,
gridFocusRequester = gridFocusRequester, gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference showJumpButtons = false, // TODO add preference

View file

@ -117,6 +117,7 @@ fun GenreCardGrid(
) )
}, },
onLongClickItem = { _, _ -> }, onLongClickItem = { _, _ -> },
onClickPlay = { _, _ -> },
letterPosition = { viewModel.positionOfLetter(it) }, letterPosition = { viewModel.positionOfLetter(it) },
gridFocusRequester = gridFocusRequester, gridFocusRequester = gridFocusRequester,
showJumpButtons = false, showJumpButtons = false,

View file

@ -124,6 +124,7 @@ fun ItemGrid(
viewModel.navigateTo(Destination.Playback(item.id, 0)) viewModel.navigateTo(Destination.Playback(item.id, 0))
}, },
onLongClickItem = { index: Int, item: BaseItem -> }, onLongClickItem = { index: Int, item: BaseItem -> },
onClickPlay = { _, item -> viewModel.navigateTo(Destination.Playback(item)) },
letterPosition = { c: Char -> 0 }, letterPosition = { c: Char -> 0 },
gridFocusRequester = focusRequester, gridFocusRequester = focusRequester,
showJumpButtons = false, showJumpButtons = false,

View file

@ -31,6 +31,7 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
@ -146,6 +147,9 @@ fun RecommendedContent(
onLongClickItem = { position, item -> onLongClickItem = { position, item ->
moreDialog.makePresent(RowColumnItem(position, item)) moreDialog.makePresent(RowColumnItem(position, item))
}, },
onClickPlay = { _, item ->
viewModel.navigationManager.navigateTo(Destination.Playback(item))
},
onFocusPosition = onFocusPosition, onFocusPosition = onFocusPosition,
showClock = preferences.appPreferences.interfacePreferences.showClock, showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier, modifier = modifier,

View file

@ -63,6 +63,7 @@ import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.playback.isBackwardButton import com.github.damontecres.wholphin.ui.playback.isBackwardButton
import com.github.damontecres.wholphin.ui.playback.isForwardButton import com.github.damontecres.wholphin.ui.playback.isForwardButton
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
import com.github.damontecres.wholphin.ui.playback.playable
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -78,6 +79,7 @@ fun CardGrid(
pager: List<BaseItem?>, pager: List<BaseItem?>,
onClickItem: (Int, BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit,
onClickPlay: (Int, BaseItem) -> Unit,
letterPosition: suspend (Char) -> Int, letterPosition: suspend (Char) -> Int,
gridFocusRequester: FocusRequester, gridFocusRequester: FocusRequester,
showJumpButtons: Boolean, showJumpButtons: Boolean,
@ -213,7 +215,11 @@ fun CardGrid(
jumpToTop() jumpToTop()
return@onKeyEvent true return@onKeyEvent true
} else if (isPlayKeyUp(it)) { } else if (isPlayKeyUp(it)) {
// TODO play the focused item val item = pager.getOrNull(focusedIndex)
if (item?.type?.playable == true) {
Timber.v("Clicked play on ${item.id}")
onClickPlay.invoke(focusedIndex, item)
}
return@onKeyEvent true return@onKeyEvent true
} else if (useJumpRemoteButtons && isForwardButton(it)) { } else if (useJumpRemoteButtons && isForwardButton(it)) {
jump(jump1) jump(jump1)

View file

@ -34,6 +34,7 @@ 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
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -67,12 +68,16 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
import com.github.damontecres.wholphin.ui.playback.playable
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber
import java.util.UUID import java.util.UUID
@Composable @Composable
@ -157,6 +162,9 @@ fun HomePage(
items = dialogItems, items = dialogItems,
) )
}, },
onClickPlay = { _, item ->
viewModel.navigationManager.navigateTo(Destination.Playback(item))
},
loadingState = refreshing, loadingState = refreshing,
showClock = preferences.appPreferences.interfacePreferences.showClock, showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier, modifier = modifier,
@ -193,6 +201,7 @@ fun HomePageContent(
homeRows: List<HomeRowLoadingState>, homeRows: List<HomeRowLoadingState>,
onClickItem: (RowColumn, BaseItem) -> Unit, onClickItem: (RowColumn, BaseItem) -> Unit,
onLongClickItem: (RowColumn, BaseItem) -> Unit, onLongClickItem: (RowColumn, BaseItem) -> Unit,
onClickPlay: (RowColumn, BaseItem) -> Unit,
showClock: Boolean, showClock: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null, onFocusPosition: ((RowColumn) -> Unit)? = null,
@ -264,7 +273,18 @@ fun HomePageContent(
top = 0.dp, top = 0.dp,
bottom = Cards.height2x3, bottom = Cards.height2x3,
), ),
modifier = Modifier.focusRestorer(), modifier =
Modifier
.focusRestorer()
.onKeyEvent {
val item = focusedItem
if (isPlayKeyUp(it) && item?.type?.playable == true) {
Timber.v("Clicked play on ${item.id}")
onClickPlay.invoke(position, item)
return@onKeyEvent true
}
return@onKeyEvent false
},
) { ) {
itemsIndexed(homeRows) { rowIndex, row -> itemsIndexed(homeRows) { rowIndex, row ->
when (val r = row) { when (val r = row) {

View file

@ -225,9 +225,13 @@ class PlaybackViewModel
"Error preparing for playback for $itemId", "Error preparing for playback for $itemId",
), ),
) { ) {
val destItem = (destination as? Destination.Playback)?.item?.data
val queriedItem = val queriedItem =
(destination as? Destination.Playback)?.item?.data if (destItem?.mediaSources != null) {
?: api.userLibraryApi.getItem(itemId).content destItem
} else {
api.userLibraryApi.getItem(itemId).content
}
val base = val base =
if (queriedItem.type.playable) { if (queriedItem.type.playable) {
queriedItem queriedItem