diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt index dbd71d4..1cd6eea 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/HomeRows.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -76,6 +77,10 @@ class HomeRowsViewModel(private val repository: PeerTubeRepository) : ViewModel( if (loaded) return loaded = true fetchContinue() + // Populate "New local videos" while the (slow, 6 detail calls) continue row is + // still resolving (#3): a user pressing DOWN too early skipped the row while it + // was empty and landed on the row below it. + recent.loadMore() } /** Re-fetch after playback changed server-side positions (UX report BUG-7). @@ -159,10 +164,21 @@ fun HomeRowsScreen( ) { val continueRestore = androidx.compose.runtime.remember { androidx.compose.runtime.mutableIntStateOf(-1) } val recentRestore = androidx.compose.runtime.remember { androidx.compose.runtime.mutableIntStateOf(-1) } + // Any D-pad press before the (slow) continue row resolves means the user is already + // navigating: suppress the then-pending auto-focus of the first row (#3). + var userNavigated by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.load() } LaunchedEffect(restoreToken) { if (restoreToken > 0) viewModel.refresh() } val recentItems = viewModel.recent.items - LazyColumn(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + // Peek at D-pad input without consuming it: marks that the user is nav. + .onPreviewKeyEvent { + userNavigated = true + false + }, + ) { item { Text( text = "Continue watching", @@ -191,12 +207,14 @@ fun HomeRowsScreen( clickedIndex = continueRestore, restoreToken = restoreToken, upToTopBar = upToTopBar, + autoFocusOnLoad = true, + autoFocusAllowed = { !userNavigated }, ) } } item { Text( - text = "New videos", + text = "New local videos", style = MaterialTheme.typography.titleMedium, color = Color.White, modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 4.dp), @@ -261,6 +279,11 @@ fun FocusScrollRow( clickedIndex: androidx.compose.runtime.MutableIntState? = null, // set on card click restoreToken: Int = 0, // bumped when playback ends upToTopBar: (() -> Unit)? = null, // first content row only + // Only the FIRST row auto-focuses its first card when items arrive — every other + // row would otherwise steal focus whenever it finishes loading (#3). + autoFocusOnLoad: Boolean = false, + // Screen-level gate: false while the user is already navigating. + autoFocusAllowed: () -> Boolean = { true }, afterItem: @Composable (index: Int, video: VideoDto) -> Unit = { _, _ -> }, ) { val rowState = rememberLazyListState() @@ -280,11 +303,14 @@ fun FocusScrollRow( } } // On app start and destination switch: focus the FIRST card automatically - // (user request — otherwise invisible focus confuses "where am I"). + // (user request — otherwise invisible focus confuses "where am I"). Rows that + // finish loading later must not steal focus from the user (#3). LaunchedEffect(videos.isNotEmpty()) { - if (videos.isNotEmpty() && focusedIndex == -1) { + if (videos.isNotEmpty() && autoFocusOnLoad && focusedIndex == -1) { kotlinx.coroutines.delay(150) // let images/measuring settle - runCatching { focusRequesters[0]?.requestFocus() } + if (autoFocusAllowed()) { + runCatching { focusRequesters[0]?.requestFocus() } + } } } LaunchedEffect(focusedIndex) {