From d5192abf62cc3234e2749c29b8aa8b6a6becfaed Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:36:24 -0500 Subject: [PATCH] Return focus to the tabs if a grid has no results (#676) ## Description Previously, if you clicked on a tab and the resulting grid had no results, the nav drawer would open and focus which is not good. This PR fixes that and instead returns focus back to the tab row. ### Related issues Fixes #543 --- .../ui/components/CollectionFolderGrid.kt | 12 ++++++++- .../wholphin/ui/components/TabRow.kt | 7 +++++- .../ui/detail/CollectionFolderLiveTv.kt | 25 +++++++++++++------ .../ui/detail/CollectionFolderMovie.kt | 4 +++ .../wholphin/ui/detail/CollectionFolderTv.kt | 3 +++ .../wholphin/ui/detail/FavoritesPage.kt | 8 ++++++ .../wholphin/ui/detail/livetv/DvrSchedule.kt | 9 ++++++- .../ui/detail/series/SeriesOverviewContent.kt | 20 ++++++++++----- 8 files changed, 71 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 20270b94..fefb4d94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -86,6 +86,7 @@ import com.github.damontecres.wholphin.ui.playback.scale import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.toServerString +import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -525,6 +526,7 @@ fun CollectionFolderGrid( positionCallback: ((columns: Int, position: Int) -> Unit)? = null, useSeriesForPrimary: Boolean = true, filterOptions: List> = DefaultFilterOptions, + focusRequesterOnEmpty: FocusRequester? = null, ) = CollectionFolderGrid( preferences, itemId.toServerString(), @@ -541,6 +543,7 @@ fun CollectionFolderGrid( positionCallback = positionCallback, useSeriesForPrimary = useSeriesForPrimary, filterOptions = filterOptions, + focusRequesterOnEmpty = focusRequesterOnEmpty, ) @Composable @@ -560,6 +563,7 @@ fun CollectionFolderGrid( positionCallback: ((columns: Int, position: Int) -> Unit)? = null, useSeriesForPrimary: Boolean = true, filterOptions: List> = DefaultFilterOptions, + focusRequesterOnEmpty: FocusRequester? = null, playlistViewModel: AddPlaylistViewModel = hiltViewModel(), viewModel: CollectionFolderViewModel = hiltViewModel( @@ -615,6 +619,7 @@ fun CollectionFolderGrid( pager = pager, sortAndDirection = sortAndDirection!!, modifier = Modifier.fillMaxSize(), + focusRequesterOnEmpty = focusRequesterOnEmpty, onClickItem = onClickItem, onLongClickItem = { position, item -> moreDialog.makePresent(PositionItem(position, item)) @@ -759,6 +764,7 @@ fun CollectionFolderGridContent( currentFilter: GetItemsFilter = GetItemsFilter(), filterOptions: List> = listOf(), onFilterChange: (GetItemsFilter) -> Unit = {}, + focusRequesterOnEmpty: FocusRequester? = null, ) { val context = LocalContext.current @@ -767,7 +773,11 @@ fun CollectionFolderGridContent( var viewOptions by remember { mutableStateOf(viewOptions) } val gridFocusRequester = remember { FocusRequester() } - RequestOrRestoreFocus(gridFocusRequester) + if (pager.isNotEmpty()) { + RequestOrRestoreFocus(gridFocusRequester) + } else { + focusRequesterOnEmpty?.tryRequestFocus() + } var backdropImageUrl by remember { mutableStateOf(null) } var position by rememberInt(initialPosition) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt index b8607b82..25b51698 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned @@ -39,13 +40,16 @@ import androidx.compose.ui.unit.sp import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.ui.PreviewTvSpec +import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus +import timber.log.Timber @Composable fun TabRow( selectedTabIndex: Int, tabs: List, + focusRequesters: List, onClick: (Int) -> Unit, modifier: Modifier = Modifier, ) { @@ -55,7 +59,6 @@ fun TabRow( state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt()) } } - val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } } var rowHasFocus by remember { mutableStateOf(false) } LazyRow( state = state, @@ -68,6 +71,7 @@ fun TabRow( onEnter = { // If entering from left or right, use last or first tab // Otherwise use the selected tab + Timber.v("onEnter requestedFocusDirection=$requestedFocusDirection, selectedTabIndex=$selectedTabIndex") val focusRequester = if (requestedFocusDirection == FocusDirection.Left) { focusRequesters.lastOrNull() @@ -183,6 +187,7 @@ private fun TabRowPreview() { TabRow( selectedTabIndex = 1, tabs = listOf("Tab 1", "Tab 2", "Tab 3"), + focusRequesters = listOf(), onClick = {}, ) Tab( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt index c71e0abe..6d1fdd02 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt @@ -93,14 +93,19 @@ fun CollectionFolderLiveTv( remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) } val folders by viewModel.recordingFolders.observeAsState(listOf()) + val tvGuideStr = stringResource(R.string.tv_guide) + val tvDvrStr = stringResource(R.string.tv_dvr_schedule) val tabs = - listOf( - TabId(stringResource(R.string.tv_guide), UUID.randomUUID()), - TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()), - ) + folders + remember(folders) { + listOf( + TabId(tvGuideStr, UUID.randomUUID()), + TabId(tvDvrStr, UUID.randomUUID()), + ) + folders + } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val tabFocusRequesters = remember(tabs.size) { List(tabs.size) { FocusRequester() } } val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } @@ -133,6 +138,7 @@ fun CollectionFolderLiveTv( .focusRequester(firstTabFocusRequester), tabs = tabs.map { it.title }, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } when (selectedTabIndex) { @@ -150,10 +156,12 @@ fun CollectionFolderLiveTv( 1 -> { DvrSchedule( - true, - Modifier - .fillMaxSize() - .focusRequester(focusRequester), + requestFocusAfterLoading = true, + focusRequesterOnEmpty = tabFocusRequesters[1], + modifier = + Modifier + .fillMaxSize() + .focusRequester(focusRequester), ) } @@ -176,6 +184,7 @@ fun CollectionFolderLiveTv( }, playEnabled = false, defaultViewOptions = ViewOptions(), + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } else { ErrorMessage("Invalid tab index $selectedTabIndex", null) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index 1292cd5c..e76b54cd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -59,6 +59,7 @@ fun CollectionFolderMovie( ) var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } @@ -88,6 +89,7 @@ fun CollectionFolderMovie( .focusRequester(firstTabFocusRequester), tabs = tabs, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } when (selectedTabIndex) { @@ -136,6 +138,7 @@ fun CollectionFolderMovie( showHeader = position < columns }, playEnabled = true, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -168,6 +171,7 @@ fun CollectionFolderMovie( showHeader = position < columns }, playEnabled = false, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt index bf032813..42b13d96 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt @@ -58,6 +58,7 @@ fun CollectionFolderTv( ) var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } @@ -91,6 +92,7 @@ fun CollectionFolderTv( .focusRequester(firstTabFocusRequester), tabs = tabs, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } when (selectedTabIndex) { @@ -138,6 +140,7 @@ fun CollectionFolderTv( preferencesViewModel.navigationManager.navigateTo(item.destination()) }, playEnabled = false, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index a62c58f5..fc337d88 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -73,6 +73,7 @@ fun FavoritesPage( stringResource(R.string.playlists), stringResource(R.string.people), ) + val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } @@ -107,6 +108,7 @@ fun FavoritesPage( .padding(start = 32.dp, top = 16.dp, bottom = 16.dp), tabs = tabs, onClick = { selectedTabIndex = it }, + focusRequesters = tabFocusRequesters, ) } // TODO playEnabled = true for movies & episodes @@ -139,6 +141,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -170,6 +173,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -202,6 +206,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -233,6 +238,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -264,6 +270,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } @@ -300,6 +307,7 @@ fun FavoritesPage( }, playEnabled = false, filterOptions = listOf(), + focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt index cfa7637b..f9758d5f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt @@ -114,6 +114,7 @@ class DvrScheduleViewModel @Composable fun DvrSchedule( requestFocusAfterLoading: Boolean, + focusRequesterOnEmpty: FocusRequester, modifier: Modifier = Modifier, viewModel: DvrScheduleViewModel = hiltViewModel(), ) { @@ -138,7 +139,13 @@ fun DvrSchedule( var showDialog by remember { mutableStateOf(null) } val focusRequester = remember { FocusRequester() } if (requestFocusAfterLoading) { - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + LaunchedEffect(Unit) { + if (active.isNotEmpty() || recordings.isNotEmpty()) { + focusRequester.tryRequestFocus() + } else { + focusRequesterOnEmpty.tryRequestFocus() + } + } } DvrScheduleContent( activeRecordings = active, 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 7f346d6c..98698826 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 @@ -107,6 +107,18 @@ fun SeriesOverviewContent( val scrollState = rememberScrollState() val scrollConnection = rememberDelayedNestedScroll() var requestFocusAfterSeason by remember { mutableStateOf(false) } + + val seasonStr = stringResource(R.string.tv_season) + val tabs = + remember(seasons) { + seasons.mapNotNull { + it?.name + ?: it?.data?.indexNumber?.let { "$seasonStr $it" } + ?: "" + } + } + val focusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } } + Box( modifier = modifier @@ -138,17 +150,13 @@ fun SeriesOverviewContent( } TabRow( selectedTabIndex = selectedTabIndex, - tabs = - seasons.mapNotNull { - it?.name - ?: it?.data?.indexNumber?.let { stringResource(R.string.tv_season) + " $it" } - ?: "" - }, + tabs = tabs, onClick = { selectedTabIndex = it onChangeSeason.invoke(it) requestFocusAfterSeason = true }, + focusRequesters = focusRequesters, modifier = Modifier .focusRequester(tabRowFocusRequester)