From 52b8cceea6abaa11270244619e14b7ff299b8b04 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 20 Oct 2025 17:23:35 -0400 Subject: [PATCH] Fix some focus issues & document it --- .../ui/detail/livetv/LiveTvViewModel.kt | 4 +- .../wholphin/ui/detail/livetv/TvGuideGrid.kt | 66 ++++++++++++++----- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index aa3a5252..263b3118 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -34,7 +34,7 @@ import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -const val MAX_HOURS = 24L +const val MAX_HOURS = 48L @HiltViewModel class LiveTvViewModel @@ -55,8 +55,6 @@ class LiveTvViewModel val fetchingItem = MutableLiveData(LoadingState.Pending) val fetchedItem = MutableLiveData(null) - private val programMap = mutableMapOf>>() - fun init() { loading.value = LoadingState.Loading viewModelScope.launch(Dispatchers.IO + LoadingExceptionHandler(loading, "Could not fetch channels")) { 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 ac26523c..74058ff8 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 @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType @@ -301,14 +302,16 @@ fun TvGuideGrid( ) } -// fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 } + var gridHasFocus by remember { mutableStateOf(false) } ProgramGuide( state = state, dimensions = dimensions, modifier = modifier - .focusable() + .onFocusChanged { + gridHasFocus = it.hasFocus + }.focusable() .onPreviewKeyEvent { if (it.type == KeyEventType.KeyUp) { return@onPreviewKeyEvent false @@ -351,35 +354,64 @@ fun TvGuideGrid( Key.DirectionUp -> { val start = programList[focusedProgramIndex].startHours - focusedChannelIndex = (focusedChannelIndex - 1) - if (focusedChannelIndex < 0) { + val newFocusedChannelIndex = (focusedChannelIndex - 1) + if (newFocusedChannelIndex < 0) { focusManager.moveFocus(FocusDirection.Up) null } else { + focusedChannelIndex = newFocusedChannelIndex.coerceAtLeast(0) val channelId = channels[focusedChannelIndex].id + val pro = programs[channelId].orEmpty() val pIndex = - programs[channelId]?.indexOfFirst { start in (it.startHours..= 0) { programsBeforeChannel.get(focusedChannelIndex) + pIndex } else { - programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size + val pIndex = pro.indexOfFirst { it.startHours >= start } + if (pIndex >= 0) { + programsBeforeChannel.get(focusedChannelIndex) + pIndex + } else { + programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size + } } } } Key.DirectionDown -> { + // When the currently focused program starts val start = programList[focusedProgramIndex].startHours - focusedChannelIndex = - (focusedChannelIndex + 1).coerceAtMost(channels.size - 1) - val channelId = channels[focusedChannelIndex].id - val pro = programs[channelId].orEmpty() - val pIndex = - pro.indexOfFirst { start in (it.startHours..= 0) { - programsBeforeChannel.get(focusedChannelIndex) + pIndex + // Move channel focus down + val newFocusedChannelIndex = (focusedChannelIndex + 1) + if (newFocusedChannelIndex >= channels.size) { + // If trying to move below the final channel, then move focus out of the grid + focusManager.moveFocus(FocusDirection.Down) + null } else { - programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size + // Otherwise, moving to a new row + focusedChannelIndex = + newFocusedChannelIndex.coerceAtMost(channels.size - 1) + // Get the new row/channel's programs + val channelId = channels[focusedChannelIndex].id + val pro = programs[channelId].orEmpty() + // Find the first program where the start time (of the previously focused program) is in the middle of a program + val pIndex = + pro.indexOfFirst { start in (it.startHours..= 0) { + // Found one, so focus on it + // Get the sum of all of the previous channels' program sizes, plus the index found to convert a relative program index into absolute + programsBeforeChannel.get(focusedChannelIndex) + pIndex + } else { + // Didn't find one, probably due to missing data + // So now first the first one that starts after the previously focused program + val pIndex = pro.indexOfFirst { it.startHours >= start } + if (pIndex >= 0) { + // Found one, so focus on it + programsBeforeChannel.get(focusedChannelIndex) + pIndex + } else { + // Did not find one, so focus on the final program in the list + programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size + } + } } } @@ -450,7 +482,7 @@ fun TvGuideGrid( }, ) { programIndex -> val program = programList[programIndex] - val focused = programIndex == focusedProgramIndex + val focused = gridHasFocus && programIndex == focusedProgramIndex val background = if (focused) { MaterialTheme.colorScheme.inverseSurface