mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 16:01:44 +00:00
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.
This commit is contained in:
parent
95ee3d1350
commit
74989b3de3
3 changed files with 58 additions and 6 deletions
|
|
@ -16,8 +16,12 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
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.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
|
|
@ -33,7 +37,9 @@ fun CatalogRowSection(
|
||||||
onItemClick: (String, String, String) -> Unit,
|
onItemClick: (String, String, String) -> Unit,
|
||||||
onLoadMore: () -> Unit,
|
onLoadMore: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
initialScrollIndex: Int = 0
|
initialScrollIndex: Int = 0,
|
||||||
|
focusedItemIndex: Int = -1,
|
||||||
|
onItemFocused: (itemIndex: Int) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState(
|
val listState = rememberLazyListState(
|
||||||
initialFirstVisibleItemIndex = initialScrollIndex
|
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 {
|
val shouldLoadMore by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||||
|
|
@ -95,7 +117,13 @@ fun CatalogRowSection(
|
||||||
ContentCard(
|
ContentCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = { onItemClick(item.id, item.type.toApiString(), catalogRow.addonBaseUrl) },
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.focus.onFocusChanged
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -40,6 +42,7 @@ import com.nuvio.tv.ui.theme.NuvioTheme
|
||||||
fun ContentCard(
|
fun ContentCard(
|
||||||
item: MetaPreview,
|
item: MetaPreview,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
focusRequester: FocusRequester? = null,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
@ -62,6 +65,10 @@ fun ContentCard(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (focusRequester != null) Modifier.focusRequester(focusRequester)
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
.onFocusChanged { isFocused = it.isFocused },
|
.onFocusChanged { isFocused = it.isFocused },
|
||||||
shape = CardDefaults.shape(
|
shape = CardDefaults.shape(
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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<String, Int>() }
|
||||||
|
|
||||||
// Save scroll position when leaving screen
|
// Save scroll position when leaving screen
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
viewModel.saveFocusState(
|
viewModel.saveFocusState(
|
||||||
verticalScrollIndex = columnListState.firstVisibleItemIndex,
|
verticalScrollIndex = columnListState.firstVisibleItemIndex,
|
||||||
verticalScrollOffset = columnListState.firstVisibleItemScrollOffset,
|
verticalScrollOffset = columnListState.firstVisibleItemScrollOffset,
|
||||||
focusedRowIndex = 0, // Basic implementation
|
focusedRowIndex = currentFocusedRowIndex,
|
||||||
focusedItemIndex = 0, // Basic implementation
|
focusedItemIndex = currentFocusedItemIndex,
|
||||||
catalogRowScrollStates = emptyMap() // Will be enhanced with horizontal scroll tracking
|
catalogRowScrollStates = catalogRowScrollStates.toMap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +117,9 @@ fun HomeScreen(
|
||||||
key = { _, item -> "${item.addonId}_${item.type}_${item.catalogId}" }
|
key = { _, item -> "${item.addonId}_${item.type}_${item.catalogId}" }
|
||||||
) { index, catalogRow ->
|
) { index, catalogRow ->
|
||||||
val catalogKey = "${catalogRow.addonId}_${catalogRow.type.toApiString()}_${catalogRow.catalogId}"
|
val catalogKey = "${catalogRow.addonId}_${catalogRow.type.toApiString()}_${catalogRow.catalogId}"
|
||||||
|
val shouldRestoreFocus = index == focusState.focusedRowIndex
|
||||||
|
val focusedItemIndex = if (shouldRestoreFocus) focusState.focusedItemIndex else -1
|
||||||
|
|
||||||
CatalogRowSection(
|
CatalogRowSection(
|
||||||
catalogRow = catalogRow,
|
catalogRow = catalogRow,
|
||||||
onItemClick = { id, type, addonBaseUrl ->
|
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
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue