ui: Explicit UP from first content row to the top bar

Focus search silently failed to move from the first content card up to
the top bar (why the menu felt unreachable); the first row's cards now
have a wired UP target (the search button) in rows and grids.
This commit is contained in:
Jeena 2026-10-11 07:31:18 +00:00
parent 828bfdafa9
commit d469101eb0
5 changed files with 26 additions and 6 deletions

View file

@ -153,6 +153,7 @@ fun HomeRowsScreen(
restoreToken: Int = 0,
epoch: Int = 0,
onOpenDrawer: () -> Unit = {},
upToTopBar: (() -> Unit)? = null,
viewModel: HomeRowsViewModel = viewModel(key = "home$epoch", factory = HomeRowsViewModel.factory),
) {
val continueRestore = androidx.compose.runtime.remember { androidx.compose.runtime.mutableIntStateOf(-1) }
@ -188,6 +189,7 @@ fun HomeRowsScreen(
onLeftAtStart = onOpenDrawer,
clickedIndex = continueRestore,
restoreToken = restoreToken,
upToTopBar = upToTopBar,
)
}
}
@ -257,6 +259,7 @@ fun FocusScrollRow(
onLeftAtStart: () -> Unit,
clickedIndex: androidx.compose.runtime.MutableIntState? = null, // set on card click
restoreToken: Int = 0, // bumped when playback ends
upToTopBar: (() -> Unit)? = null, // first content row only
afterItem: @Composable (index: Int, video: VideoDto) -> Unit = { _, _ -> },
) {
val rowState = rememberLazyListState()
@ -315,6 +318,7 @@ fun FocusScrollRow(
compact = true,
progress = progressOf(video.shortUUID),
onLeftAtStart = if (index == 0) onLeftAtStart else null,
onUpToTopBar = upToTopBar,
onFocused = { focusedIndex = index },
)
afterItem(index, video)

View file

@ -154,6 +154,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
restoreToken = restoreToken,
epoch = epoch,
onOpenDrawer = { drawerOpen = true },
upToTopBar = { runCatching { searchFocus.requestFocus() } },
)
Destination.MyVideos ->
VideoListScreen(
@ -162,6 +163,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
restoreToken = restoreToken,
epoch = epoch,
onOpenDrawer = { drawerOpen = true },
upToTopBar = { runCatching { searchFocus.requestFocus() } },
)
Destination.Channels -> ChannelsScreen(
onOpenChannel = { handle -> channelDetails = handle },
@ -175,6 +177,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
restoreToken = restoreToken,
epoch = epoch,
onOpenDrawer = { drawerOpen = true },
upToTopBar = { runCatching { searchFocus.requestFocus() } },
)
Destination.Subscriptions ->
SubscriptionRowsScreen(
@ -182,6 +185,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
restoreToken = restoreToken,
epoch = epoch,
onOpenDrawer = { drawerOpen = true },
upToTopBar = { runCatching { searchFocus.requestFocus() } },
)
Destination.History ->
VideoListScreen(
@ -190,6 +194,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
restoreToken = restoreToken,
epoch = epoch,
onOpenDrawer = { drawerOpen = true },
upToTopBar = { runCatching { searchFocus.requestFocus() } },
)
}
}

View file

@ -82,6 +82,7 @@ fun SubscriptionRowsScreen(
restoreToken: Int = 0,
epoch: Int = 0,
onOpenDrawer: () -> Unit = {},
upToTopBar: (() -> Unit)? = null,
viewModel: SubscriptionRowsViewModel = viewModel(
key = "subsrows$epoch",
factory = object : ViewModelProvider.Factory {
@ -157,6 +158,7 @@ fun SubscriptionRowsScreen(
progressOf = { null },
onOpenVideo = onOpenVideo,
onLeftAtStart = onOpenDrawer,
upToTopBar = upToTopBar.takeIf { group == groups.firstOrNull() },
) { rowIndex, _ ->
if (group.startIndex + rowIndex >= items.size - PagedListViewModel.LOOKAHEAD) {
viewModel.loadMore()

View file

@ -59,6 +59,8 @@ fun VideoCard(
compact: Boolean = false,
/** LEFT (only on the row's first card) can open the menu drawer. */
onLeftAtStart: (() -> Unit)? = null,
/** Explicit UP-target for the first content row (focus search misses the top bar). */
onUpToTopBar: (() -> Unit)? = null,
/** Notifies the row wrapper when this card gains focus (for eased scrolling). */
onFocused: (() -> Unit)? = null,
) {
@ -72,12 +74,17 @@ fun VideoCard(
if (it.isFocused) onFocused?.invoke()
}
.onPreviewKeyEvent { ke ->
if (ke.type == androidx.compose.ui.input.key.KeyEventType.KeyDown &&
ke.key == androidx.compose.ui.input.key.Key.DirectionLeft &&
onLeftAtStart != null
) {
onLeftAtStart()
true
val direction = ke.key
if (ke.type == androidx.compose.ui.input.key.KeyEventType.KeyDown) {
when {
direction == androidx.compose.ui.input.key.Key.DirectionLeft && onLeftAtStart != null -> {
onLeftAtStart(); true
}
direction == androidx.compose.ui.input.key.Key.DirectionUp && onUpToTopBar != null -> {
onUpToTopBar(); true
}
else -> false
}
} else false
},
colors = CardDefaults.colors(

View file

@ -45,6 +45,7 @@ fun VideoListScreen(
restoreToken: Int = 0,
epoch: Int = 0,
onOpenDrawer: () -> Unit = {},
upToTopBar: (() -> Unit)? = null,
viewModel: VideoListViewModel = viewModel(
key = "v$epoch:" + sourceKey(source),
factory = VideoListViewModel.factory(source),
@ -140,6 +141,7 @@ fun VideoListScreen(
showPrivacy = source is ListSource.MyVideos,
compact = true,
onLeftAtStart = if (index == 0) onOpenDrawer else null,
onUpToTopBar = upToTopBar?.takeIf { index < 4 },
modifier = Modifier
.focusRequester(fr),
)