Fixes to Now Playing page (#1915)

## Description
Fixes several issues on the Now Playing page

- Keep controller focus after lyrics are focused
- Fix the queue not visually updating until recomposition
- The first focused queue shown/focused is now what's currently playing

### Related issues
Fixes #1821

### Testing
Emulator

## Screenshots
N/A, just behavior fixes

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-09-02 02:50:42 -04:00 • committed by GitHub
parent 6fbf6209a1
commit 826bbad49c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 18 deletions

View file

@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
@ -48,6 +50,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.AudioItem import com.github.damontecres.wholphin.data.model.AudioItem
import com.github.damontecres.wholphin.services.rememberQueue
import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
@ -65,7 +68,6 @@ fun NowPlayingOverlay(
state: NowPlayingState, state: NowPlayingState,
player: Player, player: Player,
current: AudioItem?, current: AudioItem?,
queue: List<AudioItem>,
controllerViewState: ControllerViewState, controllerViewState: ControllerViewState,
onClickSong: (Int, AudioItem) -> Unit, onClickSong: (Int, AudioItem) -> Unit,
onLongClickSong: (Int, AudioItem) -> Unit, onLongClickSong: (Int, AudioItem) -> Unit,
@ -78,6 +80,7 @@ fun NowPlayingOverlay(
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val currentItemFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
var queueHasFocus by remember { mutableStateOf(false) } var queueHasFocus by remember { mutableStateOf(false) }
@ -89,13 +92,16 @@ fun NowPlayingOverlay(
}, },
animationSpec = tween(durationMillis = 500), animationSpec = tween(durationMillis = 500),
) )
val listState = rememberLazyListState() val listState =
rememberLazyListState(
initialFirstVisibleItemIndex = state.musicServiceState.currentIndex,
)
var showButtons by remember { mutableStateOf(true) } var showButtons by remember { mutableStateOf(true) }
val firstFocusRequester = remember { FocusRequester() } val firstFocusRequester = remember { FocusRequester() }
BackHandler(!showButtons) { BackHandler(!showButtons) {
scope.launch { scope.launch {
listState.animateScrollToItem(0) listState.scrollToItem(0)
firstFocusRequester.tryRequestFocus() firstFocusRequester.tryRequestFocus()
} }
} }
@ -141,6 +147,12 @@ fun NowPlayingOverlay(
.align(Alignment.CenterHorizontally), .align(Alignment.CenterHorizontally),
) )
} }
val queue =
rememberQueue(
player,
state.musicServiceState.queueVersion,
state.musicServiceState.queueSize,
)
if (queue.isEmpty()) { if (queue.isEmpty()) {
Text("No items") Text("No items")
} else { } else {
@ -158,12 +170,16 @@ fun NowPlayingOverlay(
.onFocusChanged { .onFocusChanged {
queueHasFocus = it.hasFocus queueHasFocus = it.hasFocus
}.focusProperties { }.focusProperties {
onEnter = {
currentItemFocusRequester.tryRequestFocus()
}
onExit = { onExit = {
if (requestedFocusDirection == FocusDirection.Up) focusRequester.requestFocus() if (requestedFocusDirection == FocusDirection.Up) focusRequester.requestFocus()
} }
}, },
) { ) {
itemsIndexed(queue, key = { _, song -> song.key }) { index, song -> itemsIndexed(queue, key = { _, song -> song.key }) { index, song ->
val bringIntoViewRequester = remember { BringIntoViewRequester() }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier =
@ -176,7 +192,8 @@ fun NowPlayingOverlay(
.onFocusChanged { .onFocusChanged {
if (it.hasFocus) showButtons = index < 3 if (it.hasFocus) showButtons = index < 3
controllerViewState.pulseControls() controllerViewState.pulseControls()
}.animateItem(), }.animateItem()
.bringIntoViewRequester(bringIntoViewRequester),
) { ) {
SongListItem( SongListItem(
title = song.title, title = song.title,
@ -193,6 +210,9 @@ fun NowPlayingOverlay(
.ifElse( .ifElse(
index == 0, index == 0,
Modifier.focusRequester(firstFocusRequester), Modifier.focusRequester(firstFocusRequester),
).ifElse(
index == state.musicServiceState.currentIndex,
Modifier.focusRequester(currentItemFocusRequester),
), ),
) )
Row( Row(
@ -203,12 +223,22 @@ fun NowPlayingOverlay(
MoveButton( MoveButton(
icon = R.string.fa_caret_up, icon = R.string.fa_caret_up,
enabled = index > 0, enabled = index > 0,
onClick = { onMoveQueue.invoke(index, MoveDirection.UP) }, onClick = {
onMoveQueue.invoke(index, MoveDirection.UP)
scope.launch {
bringIntoViewRequester.bringIntoView()
}
},
) )
MoveButton( MoveButton(
icon = R.string.fa_caret_down, icon = R.string.fa_caret_down,
enabled = index < queue.lastIndex, enabled = index < queue.lastIndex,
onClick = { onMoveQueue.invoke(index, MoveDirection.DOWN) }, onClick = {
onMoveQueue.invoke(index, MoveDirection.DOWN)
scope.launch {
bringIntoViewRequester.bringIntoView()
}
},
) )
Button( Button(
onClick = { onClickMoreItem.invoke(index, song) }, onClick = { onClickMoreItem.invoke(index, song) },

View file

@ -51,6 +51,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.state.rememberCurrentMediaItemState
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
@ -60,7 +61,6 @@ import com.github.damontecres.wholphin.data.model.AudioItem
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.BackdropStyle import com.github.damontecres.wholphin.preferences.BackdropStyle
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.rememberQueue
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.ContextMenu import com.github.damontecres.wholphin.ui.components.ContextMenu
import com.github.damontecres.wholphin.ui.components.ContextMenuDialog import com.github.damontecres.wholphin.ui.components.ContextMenuDialog
@ -91,13 +91,8 @@ fun NowPlayingPage(
val context = LocalContext.current val context = LocalContext.current
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val player = viewModel.player val player = viewModel.player
val queue = val currentMediaItem = rememberCurrentMediaItemState(player)
rememberQueue( val current = (currentMediaItem.mediaItem?.localConfiguration?.tag as? AudioItem)
player,
state.musicServiceState.queueVersion,
state.musicServiceState.queueSize,
)
val current = queue.getOrNull(state.musicServiceState.currentIndex)
val viz by viewModel.viz.collectAsState() val viz by viewModel.viz.collectAsState()
val controllerViewState = viewModel.controllerViewState val controllerViewState = viewModel.controllerViewState
@ -152,10 +147,12 @@ fun NowPlayingPage(
LaunchedEffect(lyricsFocused) { LaunchedEffect(lyricsFocused) {
if (lyricsFocused != null) { if (lyricsFocused != null) {
controllerViewState.hideControls() controllerViewState.hideControls()
delay(5.seconds)
if (!controllerViewState.controlsVisible) {
focusRequester.tryRequestFocus()
}
lyricsFocused = null
} }
delay(5.seconds)
lyricsFocused = null
focusRequester.tryRequestFocus()
} }
BackHandler(lyricsFocused != null) { BackHandler(lyricsFocused != null) {
focusRequester.tryRequestFocus() focusRequester.tryRequestFocus()
@ -351,7 +348,6 @@ fun NowPlayingPage(
state = state, state = state,
player = player, player = player,
current = current, current = current,
queue = queue,
controllerViewState = controllerViewState, controllerViewState = controllerViewState,
onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) }, onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) },
onClickMore = { showViewOptionsDialog = true }, onClickMore = { showViewOptionsDialog = true },