From 74989b3de3e56d0c387b4e2db40e7cf5f468e1c0 Mon Sep 17 00:00:00 2001 From: CrissZollo Date: Sun, 1 Feb 2026 23:56:11 +0100 Subject: [PATCH] Implement focus restoration for ContentCards in HomeScreen - Add FocusRequester support to ContentCard component - Track focused row and item indices in HomeScreen - Add focus restoration logic to CatalogRowSection - Wire up focus callbacks to update state on focus changes - Handle edge cases: item not composed, item no longer exists Completes focus restoration feature started in f5ead7c7. When returning from MetaDetailsScreen, focus now restores to the exact ContentCard that was clicked, not just scroll position. --- .../tv/ui/components/CatalogRowSection.kt | 32 +++++++++++++++++-- .../com/nuvio/tv/ui/components/ContentCard.kt | 7 ++++ .../nuvio/tv/ui/screens/home/HomeScreen.kt | 25 ++++++++++++--- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nuvio/tv/ui/components/CatalogRowSection.kt b/app/src/main/java/com/nuvio/tv/ui/components/CatalogRowSection.kt index 63e0cb83..58bdddb7 100644 --- a/app/src/main/java/com/nuvio/tv/ui/components/CatalogRowSection.kt +++ b/app/src/main/java/com/nuvio/tv/ui/components/CatalogRowSection.kt @@ -16,8 +16,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.tv.material3.ExperimentalTvMaterial3Api @@ -33,7 +37,9 @@ fun CatalogRowSection( onItemClick: (String, String, String) -> Unit, onLoadMore: () -> Unit, modifier: Modifier = Modifier, - initialScrollIndex: Int = 0 + initialScrollIndex: Int = 0, + focusedItemIndex: Int = -1, + onItemFocused: (itemIndex: Int) -> Unit = {} ) { val listState = rememberLazyListState( initialFirstVisibleItemIndex = initialScrollIndex @@ -46,6 +52,22 @@ fun CatalogRowSection( } } + // Track which item has focus + var currentFocusedIndex by remember { mutableStateOf(-1) } + val itemFocusRequester = remember { FocusRequester() } + + // Restore focus to specific item if requested + LaunchedEffect(focusedItemIndex) { + if (focusedItemIndex >= 0 && focusedItemIndex < catalogRow.items.size) { + kotlinx.coroutines.delay(100) // Wait for composition + try { + itemFocusRequester.requestFocus() + } catch (e: IllegalStateException) { + // Item not yet composed, ignore + } + } + } + val shouldLoadMore by remember { derivedStateOf { val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 @@ -95,7 +117,13 @@ fun CatalogRowSection( ContentCard( item = item, onClick = { onItemClick(item.id, item.type.toApiString(), catalogRow.addonBaseUrl) }, - modifier = Modifier + modifier = Modifier.onFocusChanged { focusState -> + if (focusState.isFocused) { + currentFocusedIndex = index + onItemFocused(index) + } + }, + focusRequester = if (index == focusedItemIndex) itemFocusRequester else null ) } diff --git a/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt b/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt index 25e9c4b8..fde3bfd7 100644 --- a/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt +++ b/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt @@ -18,6 +18,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow @@ -40,6 +42,7 @@ import com.nuvio.tv.ui.theme.NuvioTheme fun ContentCard( item: MetaPreview, modifier: Modifier = Modifier, + focusRequester: FocusRequester? = null, onClick: () -> Unit = {} ) { var isFocused by remember { mutableStateOf(false) } @@ -62,6 +65,10 @@ fun ContentCard( onClick = onClick, modifier = Modifier .fillMaxWidth() + .then( + if (focusRequester != null) Modifier.focusRequester(focusRequester) + else Modifier + ) .onFocusChanged { isFocused = it.isFocused }, shape = CardDefaults.shape( shape = RoundedCornerShape(8.dp) diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt index 5b2976c5..fb098313 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt @@ -13,6 +13,9 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -48,15 +51,20 @@ fun HomeScreen( } } + // Track which row and item have focus + var currentFocusedRowIndex by remember { mutableStateOf(focusState.focusedRowIndex) } + var currentFocusedItemIndex by remember { mutableStateOf(focusState.focusedItemIndex) } + val catalogRowScrollStates = remember { mutableMapOf() } + // Save scroll position when leaving screen DisposableEffect(Unit) { onDispose { viewModel.saveFocusState( verticalScrollIndex = columnListState.firstVisibleItemIndex, verticalScrollOffset = columnListState.firstVisibleItemScrollOffset, - focusedRowIndex = 0, // Basic implementation - focusedItemIndex = 0, // Basic implementation - catalogRowScrollStates = emptyMap() // Will be enhanced with horizontal scroll tracking + focusedRowIndex = currentFocusedRowIndex, + focusedItemIndex = currentFocusedItemIndex, + catalogRowScrollStates = catalogRowScrollStates.toMap() ) } } @@ -109,6 +117,9 @@ fun HomeScreen( key = { _, item -> "${item.addonId}_${item.type}_${item.catalogId}" } ) { index, catalogRow -> val catalogKey = "${catalogRow.addonId}_${catalogRow.type.toApiString()}_${catalogRow.catalogId}" + val shouldRestoreFocus = index == focusState.focusedRowIndex + val focusedItemIndex = if (shouldRestoreFocus) focusState.focusedItemIndex else -1 + CatalogRowSection( catalogRow = catalogRow, onItemClick = { id, type, addonBaseUrl -> @@ -123,7 +134,13 @@ fun HomeScreen( ) ) }, - initialScrollIndex = focusState.catalogRowScrollStates[catalogKey] ?: 0 + initialScrollIndex = focusState.catalogRowScrollStates[catalogKey] ?: 0, + focusedItemIndex = focusedItemIndex, + onItemFocused = { itemIndex -> + currentFocusedRowIndex = index + currentFocusedItemIndex = itemIndex + catalogRowScrollStates[catalogKey] = itemIndex + } ) } }