Fix TV tab selection sticking, scroll restore, and browse header clip.

Keep a single top-nav host so Films/Series stay selected, hoist list/grid state above the detail routes, and add focus padding under browse titles.
This commit is contained in:
Jos Vooges | STH 2026-09-13 00:52:44 +02:00
parent 056078d6bc
commit 94c04e0381
4 changed files with 63 additions and 25 deletions

View file

@ -20,8 +20,8 @@ android {
applicationId = "nl.vonas.mediacluster.tv"
minSdk = 24
targetSdk = 36
versionCode = 90
versionName = "0.14.27"
versionCode = 91
versionName = "0.14.28"
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")

View file

@ -62,6 +62,7 @@ fun BrowseScreen(
api: ApiClient,
catalogId: String,
titleHint: String? = null,
gridState: androidx.compose.foundation.lazy.grid.LazyGridState = rememberLazyGridState(),
onBack: (() -> Unit)? = null,
onAtTopBack: (() -> Unit)? = null,
onOpenMovie: (String) -> Unit,
@ -92,9 +93,9 @@ fun BrowseScreen(
var continueMenu by remember { mutableStateOf<MediaItem?>(null) }
var menuReady by remember { mutableStateOf(false) }
val menuFocus = remember { FocusRequester() }
val gridState = rememberLazyGridState()
val scope = rememberCoroutineScope()
val showBrowseTools = !isContinue
var lastLoadKey by rememberSaveable { mutableStateOf<String?>(null) }
suspend fun load(reset: Boolean, jumpLetter: String? = null) {
if (reset) {
@ -133,9 +134,14 @@ fun BrowseScreen(
}
LaunchedEffect(catalogId, sort) {
val key = "$catalogId|$sort"
val catalogOrSortChanged = lastLoadKey != null && lastLoadKey != key
load(reset = true, jumpLetter = null)
if (catalogOrSortChanged) {
gridState.scrollToItem(0)
}
lastLoadKey = key
}
LaunchedEffect(gridState, items.size, total, listOffset) {
snapshotFlow {
@ -259,7 +265,7 @@ fun BrowseScreen(
// ~124dp: dichter dan 140 (meer rijen), nog herkenbaar van de bank
columns = GridCells.Adaptive(minSize = 124.dp),
state = gridState,
contentPadding = PaddingValues(start = 4.dp, top = 4.dp, end = 4.dp, bottom = 28.dp),
contentPadding = PaddingValues(start = 4.dp, top = 28.dp, end = 4.dp, bottom = 28.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.fillMaxSize(),

View file

@ -56,6 +56,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@ -128,6 +129,11 @@ fun McNav() {
val hubTab = remember(hubTabName) {
MainTab.entries.firstOrNull { it.name == hubTabName } ?: MainTab.Home
}
// Scroll-state boven NavHost: blijft behouden bij film/serie/player
val homeListState = rememberLazyListState()
val moviesGridState = androidx.compose.foundation.lazy.grid.rememberLazyGridState()
val seriesGridState = androidx.compose.foundation.lazy.grid.rememberLazyGridState()
val watchlistGridState = androidx.compose.foundation.lazy.grid.rememberLazyGridState()
LaunchedEffect(Unit) {
val base = store.apiBase.first()
@ -265,6 +271,10 @@ fun McNav() {
api = api,
tab = hubTab,
onTabChange = { hubTabName = it.name },
homeListState = homeListState,
moviesGridState = moviesGridState,
seriesGridState = seriesGridState,
watchlistGridState = watchlistGridState,
tvCategoryId = tvCategoryId,
onTvCategoryChange = { tvCategoryId = it },
tvLastStreamId = tvLastStreamId,
@ -419,6 +429,10 @@ private fun MainHub(
api: ApiClient,
tab: MainTab,
onTabChange: (MainTab) -> Unit,
homeListState: androidx.compose.foundation.lazy.LazyListState,
moviesGridState: androidx.compose.foundation.lazy.grid.LazyGridState,
seriesGridState: androidx.compose.foundation.lazy.grid.LazyGridState,
watchlistGridState: androidx.compose.foundation.lazy.grid.LazyGridState,
tvCategoryId: String?,
onTvCategoryChange: (String?) -> Unit,
tvLastStreamId: Int?,
@ -447,7 +461,6 @@ private fun MainHub(
val homeContentFocus = remember { FocusRequester() }
val exitFocus = remember { FocusRequester() }
var exitReady by remember { mutableStateOf(false) }
val homeListState = rememberLazyListState()
var prevTabName by rememberSaveable { mutableStateOf<String?>(null) }
val hubScope = rememberCoroutineScope()
val activity = LocalContext.current as? Activity
@ -541,8 +554,16 @@ private fun MainHub(
focusTopNav()
}
}
// Films/Series/Zoeken: BrowseScreen handelt scroll→top af; hier alleen top-nav → Home
BackHandler(enabled = navFocused && tab != MainTab.Home && !exitConfirm) {
// Films/Series/…: Back in top-nav → Home (kort uitgeschakeld na tab-wissel tegen race)
var navBackToHomeArmed by remember { mutableStateOf(false) }
LaunchedEffect(tab, navFocused) {
navBackToHomeArmed = false
if (navFocused && tab != MainTab.Home && tab != MainTab.Settings) {
delay(400)
navBackToHomeArmed = true
}
}
BackHandler(enabled = navBackToHomeArmed && navFocused && tab != MainTab.Home && !exitConfirm) {
onTabChange(MainTab.Home)
focusTopNav()
}
@ -581,6 +602,12 @@ private fun MainHub(
}
val homeOverlay = tab == MainTab.Home
val density = LocalDensity.current
var topNavHeightPx by remember { mutableIntStateOf(0) }
val topNavBlockHeight = with(density) {
if (topNavHeightPx > 0) topNavHeightPx.toDp()
else nl.vonas.mediacluster.tv.ui.components.HomeChrome.TopNavHeight
}
@Composable
fun HubTopNav(overlay: Boolean) {
@ -660,10 +687,9 @@ private fun MainHub(
Box(Modifier.fillMaxSize().background(McColors.Bg)) {
Column(Modifier.fillMaxSize()) {
// Vaste ruimte voor topnav (niet op Home: daar ligt nav als overlay)
if (!homeOverlay) {
Box(Modifier.focusRequester(navFocus)) {
HubTopNav(overlay = false)
}
Spacer(Modifier.height(topNavBlockHeight))
}
AnimatedContent(
targetState = tab,
@ -688,6 +714,7 @@ private fun MainHub(
)
MainTab.Watchlist -> WatchlistScreen(
api = api,
gridState = watchlistGridState,
onAtTopBack = { focusTopNav() },
onOpenMovie = onOpenMovie,
onOpenSeries = onOpenSeries,
@ -696,6 +723,7 @@ private fun MainHub(
api = api,
catalogId = movieCatalog,
titleHint = movieSubs.firstOrNull { it.id == movieSubId }?.label ?: "Films",
gridState = moviesGridState,
onAtTopBack = { focusTopNav() },
onOpenMovie = onOpenMovie,
onOpenSeries = onOpenSeries,
@ -704,6 +732,7 @@ private fun MainHub(
api = api,
catalogId = seriesCatalog,
titleHint = seriesSubs.firstOrNull { it.id == seriesSubId }?.label ?: "Series",
gridState = seriesGridState,
onAtTopBack = { focusTopNav() },
onOpenMovie = onOpenMovie,
onOpenSeries = onOpenSeries,
@ -744,15 +773,15 @@ private fun MainHub(
}
}
if (homeOverlay) {
// Eén vaste TopNav-host: blijft bestaan bij Home↔Films (voorkomt tab-reset)
Box(
Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
.focusRequester(navFocus),
.focusRequester(navFocus)
.onSizeChanged { topNavHeightPx = it.height },
) {
HubTopNav(overlay = true)
}
HubTopNav(overlay = homeOverlay)
}
if (exitConfirm) {
@ -1824,6 +1853,8 @@ private fun SettingsScreen(
@Composable
private fun WatchlistScreen(
api: ApiClient,
gridState: androidx.compose.foundation.lazy.grid.LazyGridState =
androidx.compose.foundation.lazy.grid.rememberLazyGridState(),
onAtTopBack: () -> Unit,
onOpenMovie: (String) -> Unit,
onOpenSeries: (String) -> Unit,
@ -1871,6 +1902,7 @@ private fun WatchlistScreen(
else -> {
androidx.compose.foundation.lazy.grid.LazyVerticalGrid(
columns = androidx.compose.foundation.lazy.grid.GridCells.Adaptive(minSize = 124.dp),
state = gridState,
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
contentPadding = PaddingValues(top = 28.dp, bottom = 32.dp),

View file

@ -106,12 +106,12 @@ fun TopNavBar(
Modifier
},
)
// Semi-transparant of vast: home overlay vs vaste tabs
Column(
modifier
.fillMaxWidth()
.then(
if (overlay) {
// Semi-transparant: hero zichtbaar, menu blijft leesbaar
Modifier.background(
Brush.verticalGradient(
0f to Color(0xB8000000),
@ -120,7 +120,7 @@ fun TopNavBar(
),
)
} else {
Modifier
Modifier.background(McColors.Bg)
},
)
.graphicsLayer { this.alpha = alpha },
@ -232,9 +232,9 @@ fun TopNavBar(
if (!profileLabel.isNullOrBlank()) {
Text(
profileLabel,
color = McColors.Brand,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
fontWeight = FontWeight.Medium,
maxLines = 1,
)
}