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:
parent
056078d6bc
commit
94c04e0381
4 changed files with 63 additions and 25 deletions
|
|
@ -20,8 +20,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster.tv"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 90
|
versionCode = 91
|
||||||
versionName = "0.14.27"
|
versionName = "0.14.28"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ fun BrowseScreen(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
catalogId: String,
|
catalogId: String,
|
||||||
titleHint: String? = null,
|
titleHint: String? = null,
|
||||||
|
gridState: androidx.compose.foundation.lazy.grid.LazyGridState = rememberLazyGridState(),
|
||||||
onBack: (() -> Unit)? = null,
|
onBack: (() -> Unit)? = null,
|
||||||
onAtTopBack: (() -> Unit)? = null,
|
onAtTopBack: (() -> Unit)? = null,
|
||||||
onOpenMovie: (String) -> Unit,
|
onOpenMovie: (String) -> Unit,
|
||||||
|
|
@ -92,9 +93,9 @@ fun BrowseScreen(
|
||||||
var continueMenu by remember { mutableStateOf<MediaItem?>(null) }
|
var continueMenu by remember { mutableStateOf<MediaItem?>(null) }
|
||||||
var menuReady by remember { mutableStateOf(false) }
|
var menuReady by remember { mutableStateOf(false) }
|
||||||
val menuFocus = remember { FocusRequester() }
|
val menuFocus = remember { FocusRequester() }
|
||||||
val gridState = rememberLazyGridState()
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val showBrowseTools = !isContinue
|
val showBrowseTools = !isContinue
|
||||||
|
var lastLoadKey by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
suspend fun load(reset: Boolean, jumpLetter: String? = null) {
|
suspend fun load(reset: Boolean, jumpLetter: String? = null) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
|
|
@ -133,9 +134,14 @@ fun BrowseScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(catalogId, sort) {
|
LaunchedEffect(catalogId, sort) {
|
||||||
|
val key = "$catalogId|$sort"
|
||||||
|
val catalogOrSortChanged = lastLoadKey != null && lastLoadKey != key
|
||||||
load(reset = true, jumpLetter = null)
|
load(reset = true, jumpLetter = null)
|
||||||
|
if (catalogOrSortChanged) {
|
||||||
gridState.scrollToItem(0)
|
gridState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
|
lastLoadKey = key
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(gridState, items.size, total, listOffset) {
|
LaunchedEffect(gridState, items.size, total, listOffset) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
|
|
@ -259,7 +265,7 @@ fun BrowseScreen(
|
||||||
// ~124dp: dichter dan 140 (meer rijen), nog herkenbaar van de bank
|
// ~124dp: dichter dan 140 (meer rijen), nog herkenbaar van de bank
|
||||||
columns = GridCells.Adaptive(minSize = 124.dp),
|
columns = GridCells.Adaptive(minSize = 124.dp),
|
||||||
state = gridState,
|
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),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
|
@ -128,6 +129,11 @@ fun McNav() {
|
||||||
val hubTab = remember(hubTabName) {
|
val hubTab = remember(hubTabName) {
|
||||||
MainTab.entries.firstOrNull { it.name == hubTabName } ?: MainTab.Home
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
val base = store.apiBase.first()
|
val base = store.apiBase.first()
|
||||||
|
|
@ -265,6 +271,10 @@ fun McNav() {
|
||||||
api = api,
|
api = api,
|
||||||
tab = hubTab,
|
tab = hubTab,
|
||||||
onTabChange = { hubTabName = it.name },
|
onTabChange = { hubTabName = it.name },
|
||||||
|
homeListState = homeListState,
|
||||||
|
moviesGridState = moviesGridState,
|
||||||
|
seriesGridState = seriesGridState,
|
||||||
|
watchlistGridState = watchlistGridState,
|
||||||
tvCategoryId = tvCategoryId,
|
tvCategoryId = tvCategoryId,
|
||||||
onTvCategoryChange = { tvCategoryId = it },
|
onTvCategoryChange = { tvCategoryId = it },
|
||||||
tvLastStreamId = tvLastStreamId,
|
tvLastStreamId = tvLastStreamId,
|
||||||
|
|
@ -419,6 +429,10 @@ private fun MainHub(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
tab: MainTab,
|
tab: MainTab,
|
||||||
onTabChange: (MainTab) -> Unit,
|
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?,
|
tvCategoryId: String?,
|
||||||
onTvCategoryChange: (String?) -> Unit,
|
onTvCategoryChange: (String?) -> Unit,
|
||||||
tvLastStreamId: Int?,
|
tvLastStreamId: Int?,
|
||||||
|
|
@ -447,7 +461,6 @@ private fun MainHub(
|
||||||
val homeContentFocus = remember { FocusRequester() }
|
val homeContentFocus = remember { FocusRequester() }
|
||||||
val exitFocus = remember { FocusRequester() }
|
val exitFocus = remember { FocusRequester() }
|
||||||
var exitReady by remember { mutableStateOf(false) }
|
var exitReady by remember { mutableStateOf(false) }
|
||||||
val homeListState = rememberLazyListState()
|
|
||||||
var prevTabName by rememberSaveable { mutableStateOf<String?>(null) }
|
var prevTabName by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val hubScope = rememberCoroutineScope()
|
val hubScope = rememberCoroutineScope()
|
||||||
val activity = LocalContext.current as? Activity
|
val activity = LocalContext.current as? Activity
|
||||||
|
|
@ -541,8 +554,16 @@ private fun MainHub(
|
||||||
focusTopNav()
|
focusTopNav()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Films/Series/Zoeken: BrowseScreen handelt scroll→top af; hier alleen top-nav → Home
|
// Films/Series/…: Back in top-nav → Home (kort uitgeschakeld na tab-wissel tegen race)
|
||||||
BackHandler(enabled = navFocused && tab != MainTab.Home && !exitConfirm) {
|
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)
|
onTabChange(MainTab.Home)
|
||||||
focusTopNav()
|
focusTopNav()
|
||||||
}
|
}
|
||||||
|
|
@ -581,6 +602,12 @@ private fun MainHub(
|
||||||
}
|
}
|
||||||
|
|
||||||
val homeOverlay = tab == MainTab.Home
|
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
|
@Composable
|
||||||
fun HubTopNav(overlay: Boolean) {
|
fun HubTopNav(overlay: Boolean) {
|
||||||
|
|
@ -660,10 +687,9 @@ private fun MainHub(
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize().background(McColors.Bg)) {
|
Box(Modifier.fillMaxSize().background(McColors.Bg)) {
|
||||||
Column(Modifier.fillMaxSize()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
|
// Vaste ruimte voor topnav (niet op Home: daar ligt nav als overlay)
|
||||||
if (!homeOverlay) {
|
if (!homeOverlay) {
|
||||||
Box(Modifier.focusRequester(navFocus)) {
|
Spacer(Modifier.height(topNavBlockHeight))
|
||||||
HubTopNav(overlay = false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = tab,
|
targetState = tab,
|
||||||
|
|
@ -688,6 +714,7 @@ private fun MainHub(
|
||||||
)
|
)
|
||||||
MainTab.Watchlist -> WatchlistScreen(
|
MainTab.Watchlist -> WatchlistScreen(
|
||||||
api = api,
|
api = api,
|
||||||
|
gridState = watchlistGridState,
|
||||||
onAtTopBack = { focusTopNav() },
|
onAtTopBack = { focusTopNav() },
|
||||||
onOpenMovie = onOpenMovie,
|
onOpenMovie = onOpenMovie,
|
||||||
onOpenSeries = onOpenSeries,
|
onOpenSeries = onOpenSeries,
|
||||||
|
|
@ -696,6 +723,7 @@ private fun MainHub(
|
||||||
api = api,
|
api = api,
|
||||||
catalogId = movieCatalog,
|
catalogId = movieCatalog,
|
||||||
titleHint = movieSubs.firstOrNull { it.id == movieSubId }?.label ?: "Films",
|
titleHint = movieSubs.firstOrNull { it.id == movieSubId }?.label ?: "Films",
|
||||||
|
gridState = moviesGridState,
|
||||||
onAtTopBack = { focusTopNav() },
|
onAtTopBack = { focusTopNav() },
|
||||||
onOpenMovie = onOpenMovie,
|
onOpenMovie = onOpenMovie,
|
||||||
onOpenSeries = onOpenSeries,
|
onOpenSeries = onOpenSeries,
|
||||||
|
|
@ -704,6 +732,7 @@ private fun MainHub(
|
||||||
api = api,
|
api = api,
|
||||||
catalogId = seriesCatalog,
|
catalogId = seriesCatalog,
|
||||||
titleHint = seriesSubs.firstOrNull { it.id == seriesSubId }?.label ?: "Series",
|
titleHint = seriesSubs.firstOrNull { it.id == seriesSubId }?.label ?: "Series",
|
||||||
|
gridState = seriesGridState,
|
||||||
onAtTopBack = { focusTopNav() },
|
onAtTopBack = { focusTopNav() },
|
||||||
onOpenMovie = onOpenMovie,
|
onOpenMovie = onOpenMovie,
|
||||||
onOpenSeries = onOpenSeries,
|
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(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(navFocus),
|
.focusRequester(navFocus)
|
||||||
|
.onSizeChanged { topNavHeightPx = it.height },
|
||||||
) {
|
) {
|
||||||
HubTopNav(overlay = true)
|
HubTopNav(overlay = homeOverlay)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exitConfirm) {
|
if (exitConfirm) {
|
||||||
|
|
@ -1824,6 +1853,8 @@ private fun SettingsScreen(
|
||||||
@Composable
|
@Composable
|
||||||
private fun WatchlistScreen(
|
private fun WatchlistScreen(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
|
gridState: androidx.compose.foundation.lazy.grid.LazyGridState =
|
||||||
|
androidx.compose.foundation.lazy.grid.rememberLazyGridState(),
|
||||||
onAtTopBack: () -> Unit,
|
onAtTopBack: () -> Unit,
|
||||||
onOpenMovie: (String) -> Unit,
|
onOpenMovie: (String) -> Unit,
|
||||||
onOpenSeries: (String) -> Unit,
|
onOpenSeries: (String) -> Unit,
|
||||||
|
|
@ -1871,6 +1902,7 @@ private fun WatchlistScreen(
|
||||||
else -> {
|
else -> {
|
||||||
androidx.compose.foundation.lazy.grid.LazyVerticalGrid(
|
androidx.compose.foundation.lazy.grid.LazyVerticalGrid(
|
||||||
columns = androidx.compose.foundation.lazy.grid.GridCells.Adaptive(minSize = 124.dp),
|
columns = androidx.compose.foundation.lazy.grid.GridCells.Adaptive(minSize = 124.dp),
|
||||||
|
state = gridState,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
contentPadding = PaddingValues(top = 28.dp, bottom = 32.dp),
|
contentPadding = PaddingValues(top = 28.dp, bottom = 32.dp),
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,12 @@ fun TopNavBar(
|
||||||
Modifier
|
Modifier
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// Semi-transparant of vast: home overlay vs vaste tabs
|
||||||
Column(
|
Column(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(
|
.then(
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
// Semi-transparant: hero zichtbaar, menu blijft leesbaar
|
|
||||||
Modifier.background(
|
Modifier.background(
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
0f to Color(0xB8000000),
|
0f to Color(0xB8000000),
|
||||||
|
|
@ -120,7 +120,7 @@ fun TopNavBar(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier.background(McColors.Bg)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.graphicsLayer { this.alpha = alpha },
|
.graphicsLayer { this.alpha = alpha },
|
||||||
|
|
@ -232,9 +232,9 @@ fun TopNavBar(
|
||||||
if (!profileLabel.isNullOrBlank()) {
|
if (!profileLabel.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
profileLabel,
|
profileLabel,
|
||||||
color = McColors.Brand,
|
color = Color.White,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.Medium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue