fix Events chips focus, default Back on upcoming detail, restore Nu live rail
This commit is contained in:
parent
55710da15b
commit
ccb42db2a1
3 changed files with 99 additions and 55 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 = 125
|
versionCode = 126
|
||||||
versionName = "0.14.62"
|
versionName = "0.14.63"
|
||||||
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")
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,9 @@ fun EventsScreen(
|
||||||
var providerFilter by remember { mutableStateOf<String?>(null) }
|
var providerFilter by remember { mutableStateOf<String?>(null) }
|
||||||
var nowMs by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
var nowMs by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||||
var chipsFocusReady by remember { mutableStateOf(false) }
|
var chipsFocusReady by remember { mutableStateOf(false) }
|
||||||
var railFocusReady by remember { mutableStateOf(false) }
|
/** Owner-id van first-rail FocusRequester — voorkomt dispose-race live↔upcoming. */
|
||||||
|
var railFocusOwner by remember { mutableStateOf<String?>(null) }
|
||||||
|
val railFocusReady = railFocusOwner != null
|
||||||
var restoreCardReady by remember { mutableStateOf(false) }
|
var restoreCardReady by remember { mutableStateOf(false) }
|
||||||
var placeholderHadFocus by remember { mutableStateOf(false) }
|
var placeholderHadFocus by remember { mutableStateOf(false) }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
@ -135,31 +137,56 @@ fun EventsScreen(
|
||||||
if (providerFilter.isNullOrBlank()) events
|
if (providerFilter.isNullOrBlank()) events
|
||||||
else events.filter { it.provider.equals(providerFilter, ignoreCase = true) }
|
else events.filter { it.provider.equals(providerFilter, ignoreCase = true) }
|
||||||
}
|
}
|
||||||
val live = remember(filtered) { filtered.filter { it.phase == "live" || it.isLive } }
|
// Client-side time window + API flags — geen hero-art filter op rails.
|
||||||
|
val live = remember(filtered, nowMs) {
|
||||||
|
filtered.filter { eventIsLive(it, nowMs) }
|
||||||
|
.sortedByDescending { it.playable }
|
||||||
|
}
|
||||||
val byDay = remember(filtered, nowMs) { groupEventsByDay(filtered) }
|
val byDay = remember(filtered, nowMs) { groupEventsByDay(filtered) }
|
||||||
val providers = remember(events) {
|
val providers = remember(events) {
|
||||||
events.map { it.provider }.distinct().sorted()
|
events.map { it.provider }.distinct().sorted()
|
||||||
}
|
}
|
||||||
val showProviderChips = providers.size > 1
|
val showProviderChips = providers.size > 1
|
||||||
val upcomingRails = remember(byDay) {
|
val upcomingRails = remember(byDay, nowMs) {
|
||||||
byDay.mapNotNull { (label, dayEvents) ->
|
byDay.mapNotNull { (label, dayEvents) ->
|
||||||
val upcoming = dayEvents.filter { it.phase != "live" && !it.isLive }
|
val upcoming = dayEvents.filter { !eventIsLive(it, nowMs) }
|
||||||
if (upcoming.isEmpty()) null else label to upcoming
|
if (upcoming.isEmpty()) null else label to upcoming
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val hasContent = filtered.isNotEmpty()
|
val hasContent = filtered.isNotEmpty()
|
||||||
val hasRails = live.isNotEmpty() || upcomingRails.isNotEmpty()
|
val hasRails = live.isNotEmpty() || upcomingRails.isNotEmpty()
|
||||||
|
val firstRailOwnerId = when {
|
||||||
|
live.isNotEmpty() -> "live"
|
||||||
|
upcomingRails.isNotEmpty() -> "day-${upcomingRails.first().first}"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
// Focus: TopNav → chips (optioneel) → first rail card
|
// Focus: TopNav → chips (optioneel) → first rail card.
|
||||||
val chipsDownTarget = firstRailFocus.takeIf { railFocusReady }
|
// Nooit focusProperties.down op firstRailFocus (stale/uninitialized na filter) —
|
||||||
|
// chips gebruiken altijd onFocusDownFallback → focusFirstRail().
|
||||||
val firstRailUpTarget = when {
|
val firstRailUpTarget = when {
|
||||||
showProviderChips && chipsFocusReady -> contentFocusRequester
|
showProviderChips && chipsFocusReady -> contentFocusRequester
|
||||||
else -> navFocusRequester
|
else -> navFocusRequester
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun claimRailFocus(owner: String, ready: Boolean) {
|
||||||
|
if (ready) {
|
||||||
|
if (owner == firstRailOwnerId) railFocusOwner = owner
|
||||||
|
} else if (railFocusOwner == owner) {
|
||||||
|
railFocusOwner = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter-wissel: scroll omhoog (Nu live zichtbaar) + forceer re-bind first rail.
|
||||||
|
LaunchedEffect(providerFilter) {
|
||||||
|
railFocusOwner = null
|
||||||
|
runCatching { listState.scrollToItem(0) }
|
||||||
|
}
|
||||||
|
|
||||||
val firstRailListIndex = remember(showProviderChips, live, upcomingRails) {
|
val firstRailListIndex = remember(showProviderChips, live, upcomingRails) {
|
||||||
if (live.isEmpty() && upcomingRails.isEmpty()) return@remember -1
|
if (live.isEmpty() && upcomingRails.isEmpty()) return@remember -1
|
||||||
var idx = 1 // after top spacer
|
// Geen hero/top-spacer meer: index 0 = chips of eerste rail.
|
||||||
|
var idx = 0
|
||||||
if (showProviderChips) idx++
|
if (showProviderChips) idx++
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +207,7 @@ fun EventsScreen(
|
||||||
restoreUpcomingIndex,
|
restoreUpcomingIndex,
|
||||||
) {
|
) {
|
||||||
if (!canRestoreCard) return@remember -1
|
if (!canRestoreCard) return@remember -1
|
||||||
var idx = 1 // after top spacer
|
var idx = 0
|
||||||
if (showProviderChips) idx++
|
if (showProviderChips) idx++
|
||||||
if (restoreInLive) return@remember idx
|
if (restoreInLive) return@remember idx
|
||||||
if (live.isNotEmpty()) idx++
|
if (live.isNotEmpty()) idx++
|
||||||
|
|
@ -223,14 +250,13 @@ fun EventsScreen(
|
||||||
|
|
||||||
fun focusFirstRail() {
|
fun focusFirstRail() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (!awaitReady({ railFocusReady })) return@launch
|
runCatching { listState.scrollToItem(0) }
|
||||||
runCatching {
|
if (firstRailListIndex > 0) {
|
||||||
if (firstRailListIndex >= 0) {
|
runCatching { listState.scrollToItem(firstRailListIndex) }
|
||||||
listState.animateScrollToItem(firstRailListIndex)
|
}
|
||||||
|
if (!awaitReady({ railFocusReady }, timeoutMs = 1200)) return@launch
|
||||||
delay(40)
|
delay(40)
|
||||||
}
|
runCatching { firstRailFocus.requestFocus() }
|
||||||
firstRailFocus.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -359,9 +385,7 @@ fun EventsScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(bottom = 48.dp),
|
contentPadding = PaddingValues(bottom = 48.dp),
|
||||||
) {
|
) {
|
||||||
item(key = "top-spacer") {
|
// TopNav reserveert al ruimte (niet-overlay) — geen BadgeTop-spacer.
|
||||||
Spacer(Modifier.height(HomeChrome.BadgeTop + 24.dp))
|
|
||||||
}
|
|
||||||
if (showProviderChips) {
|
if (showProviderChips) {
|
||||||
item(key = "providers") {
|
item(key = "providers") {
|
||||||
ProviderChips(
|
ProviderChips(
|
||||||
|
|
@ -370,14 +394,10 @@ fun EventsScreen(
|
||||||
onSelect = { providerFilter = it },
|
onSelect = { providerFilter = it },
|
||||||
firstFocusRequester = contentFocusRequester,
|
firstFocusRequester = contentFocusRequester,
|
||||||
focusUp = navFocusRequester,
|
focusUp = navFocusRequester,
|
||||||
focusDown = chipsDownTarget,
|
|
||||||
onFocusReady = { chipsFocusReady = it },
|
onFocusReady = { chipsFocusReady = it },
|
||||||
onFocus = onContentFocused,
|
onFocus = onContentFocused,
|
||||||
onFocusDownFallback = if (chipsDownTarget == null) {
|
// Altijd expliciet requestFocus — nooit stale focusProperties.down.
|
||||||
({ focusFirstRail() })
|
onFocusDownFallback = { focusFirstRail() },
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -400,12 +420,15 @@ fun EventsScreen(
|
||||||
},
|
},
|
||||||
onRestoreFocusReady = { restoreCardReady = it },
|
onRestoreFocusReady = { restoreCardReady = it },
|
||||||
focusUpTo = firstRailUpTarget,
|
focusUpTo = firstRailUpTarget,
|
||||||
onFirstFocusReady = { railFocusReady = it },
|
onFirstFocusReady = { ready ->
|
||||||
|
claimRailFocus("live", ready)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
upcomingRails.forEachIndexed { index, (label, dayEvents) ->
|
upcomingRails.forEachIndexed { index, (label, dayEvents) ->
|
||||||
val isFirstUpcoming = live.isEmpty() && index == 0
|
val isFirstUpcoming = live.isEmpty() && index == 0
|
||||||
|
val ownerId = "day-$label"
|
||||||
val railHasRestore = bindRestoreCardId != null &&
|
val railHasRestore = bindRestoreCardId != null &&
|
||||||
!restoreInLive &&
|
!restoreInLive &&
|
||||||
dayEvents.any { it.id == bindRestoreCardId }
|
dayEvents.any { it.id == bindRestoreCardId }
|
||||||
|
|
@ -427,8 +450,10 @@ fun EventsScreen(
|
||||||
if (railHasRestore) restoreCardReady = ready
|
if (railHasRestore) restoreCardReady = ready
|
||||||
},
|
},
|
||||||
focusUpTo = firstRailUpTarget.takeIf { isFirstUpcoming },
|
focusUpTo = firstRailUpTarget.takeIf { isFirstUpcoming },
|
||||||
onFirstFocusReady = { ready ->
|
onFirstFocusReady = if (isFirstUpcoming) {
|
||||||
if (isFirstUpcoming) railFocusReady = ready
|
{ ready -> claimRailFocus(ownerId, ready) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -604,22 +629,27 @@ fun EventDetailScreen(
|
||||||
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
||||||
onClick = { onPlay(ev.id) },
|
onClick = { onPlay(ev.id) },
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
McDetailButton(
|
|
||||||
ctaLabel(ev, nowMs),
|
|
||||||
primary = false,
|
|
||||||
enabled = false,
|
|
||||||
focusRequester = playFocus,
|
|
||||||
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
|
||||||
onClick = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
McDetailButton(
|
McDetailButton(
|
||||||
"Terug",
|
"Terug",
|
||||||
focusRequester = backFocus,
|
focusRequester = backFocus,
|
||||||
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Disabled CTA links, buiten focus-order; default focus op Terug.
|
||||||
|
McDetailButton(
|
||||||
|
ctaLabel(ev, nowMs),
|
||||||
|
primary = false,
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
McDetailButton(
|
||||||
|
"Terug",
|
||||||
|
focusRequester = playFocus,
|
||||||
|
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
||||||
|
onClick = onBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFocusableBody) {
|
if (hasFocusableBody) {
|
||||||
|
|
@ -735,7 +765,7 @@ private fun EventRail(
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (isFirst && firstItemFocusRequester != null && cardRequester === firstItemFocusRequester) {
|
if (isFirst && firstItemFocusRequester != null && cardRequester === firstItemFocusRequester) {
|
||||||
DisposableEffect(firstItemFocusRequester) {
|
DisposableEffect(firstItemFocusRequester, events.firstOrNull()?.id) {
|
||||||
onFirstFocusReady?.invoke(true)
|
onFirstFocusReady?.invoke(true)
|
||||||
onDispose { onFirstFocusReady?.invoke(false) }
|
onDispose { onFirstFocusReady?.invoke(false) }
|
||||||
}
|
}
|
||||||
|
|
@ -873,14 +903,17 @@ private fun ProviderChips(
|
||||||
onSelect: (String?) -> Unit,
|
onSelect: (String?) -> Unit,
|
||||||
firstFocusRequester: FocusRequester? = null,
|
firstFocusRequester: FocusRequester? = null,
|
||||||
focusUp: FocusRequester? = null,
|
focusUp: FocusRequester? = null,
|
||||||
focusDown: FocusRequester? = null,
|
|
||||||
onFocusReady: ((Boolean) -> Unit)? = null,
|
onFocusReady: ((Boolean) -> Unit)? = null,
|
||||||
onFocus: () -> Unit = {},
|
onFocus: () -> Unit = {},
|
||||||
onFocusDownFallback: (() -> Unit)? = null,
|
onFocusDownFallback: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
// Mirror TopNav subItems (Films/Series): EdgeInset + vertical 4.dp, bottom 8.dp.
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset, vertical = 14.dp),
|
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset, vertical = 4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Chip(
|
Chip(
|
||||||
|
|
@ -888,7 +921,6 @@ private fun ProviderChips(
|
||||||
active = selected == null,
|
active = selected == null,
|
||||||
focusRequester = firstFocusRequester,
|
focusRequester = firstFocusRequester,
|
||||||
focusUp = focusUp,
|
focusUp = focusUp,
|
||||||
focusDown = focusDown,
|
|
||||||
onFocusReady = onFocusReady,
|
onFocusReady = onFocusReady,
|
||||||
onFocus = onFocus,
|
onFocus = onFocus,
|
||||||
onFocusDownFallback = onFocusDownFallback,
|
onFocusDownFallback = onFocusDownFallback,
|
||||||
|
|
@ -900,7 +932,6 @@ private fun ProviderChips(
|
||||||
label = prettyProvider(p),
|
label = prettyProvider(p),
|
||||||
active = selected.equals(p, ignoreCase = true),
|
active = selected.equals(p, ignoreCase = true),
|
||||||
focusUp = focusUp,
|
focusUp = focusUp,
|
||||||
focusDown = focusDown,
|
|
||||||
onFocus = onFocus,
|
onFocus = onFocus,
|
||||||
onFocusDownFallback = onFocusDownFallback,
|
onFocusDownFallback = onFocusDownFallback,
|
||||||
onClick = { onSelect(p) },
|
onClick = { onSelect(p) },
|
||||||
|
|
@ -1018,15 +1049,17 @@ private fun McDetailButton(
|
||||||
val mod = Modifier
|
val mod = Modifier
|
||||||
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
|
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
|
||||||
.then(
|
.then(
|
||||||
if (focusUp != null || (focusDown != null && onFocusDownFallback == null)) {
|
when {
|
||||||
|
!enabled -> Modifier.focusProperties { canFocus = false }
|
||||||
|
focusUp != null || (focusDown != null && onFocusDownFallback == null) -> {
|
||||||
Modifier.focusProperties {
|
Modifier.focusProperties {
|
||||||
focusUp?.let { up = it }
|
focusUp?.let { up = it }
|
||||||
if (onFocusDownFallback == null) {
|
if (onFocusDownFallback == null) {
|
||||||
focusDown?.let { down = it }
|
focusDown?.let { down = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Modifier
|
else -> Modifier
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
|
|
@ -1093,7 +1126,7 @@ private fun McDetailButton(
|
||||||
|
|
||||||
private fun statusVisual(event: ScheduleEvent, nowMs: Long): Pair<String, Color> {
|
private fun statusVisual(event: ScheduleEvent, nowMs: Long): Pair<String, Color> {
|
||||||
return when {
|
return when {
|
||||||
event.phase == "live" || event.isLive -> "LIVE" to McColors.Accent
|
eventIsLive(event, nowMs) -> "LIVE" to McColors.Accent
|
||||||
minutesUntilStart(event, nowMs)?.let { it in 0 until 60 } == true -> {
|
minutesUntilStart(event, nowMs)?.let { it in 0 until 60 } == true -> {
|
||||||
val m = minutesUntilStart(event, nowMs)!!
|
val m = minutesUntilStart(event, nowMs)!!
|
||||||
"Straks · over ${m} min" to Color(0xFFE67E22)
|
"Straks · over ${m} min" to Color(0xFFE67E22)
|
||||||
|
|
@ -1103,7 +1136,7 @@ private fun statusVisual(event: ScheduleEvent, nowMs: Long): Pair<String, Color>
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ctaLabel(event: ScheduleEvent, nowMs: Long): String {
|
private fun ctaLabel(event: ScheduleEvent, nowMs: Long): String {
|
||||||
if (event.phase == "live" || event.isLive) return "Stream start binnenkort"
|
if (eventIsLive(event, nowMs)) return "Stream start binnenkort"
|
||||||
val mins = minutesUntilStart(event, nowMs)
|
val mins = minutesUntilStart(event, nowMs)
|
||||||
if (mins != null && mins in 0 until 60) return "Start over $mins min"
|
if (mins != null && mins in 0 until 60) return "Start over $mins min"
|
||||||
val start = parseZoned(event.startLocal, event.start) ?: return "Meer info"
|
val start = parseZoned(event.startLocal, event.start) ?: return "Meer info"
|
||||||
|
|
@ -1111,7 +1144,7 @@ private fun ctaLabel(event: ScheduleEvent, nowMs: Long): String {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shortTimeLabel(event: ScheduleEvent, nowMs: Long): String {
|
private fun shortTimeLabel(event: ScheduleEvent, nowMs: Long): String {
|
||||||
if (event.phase == "live" || event.isLive) {
|
if (eventIsLive(event, nowMs)) {
|
||||||
val left = minutesUntilEnd(event, nowMs)
|
val left = minutesUntilEnd(event, nowMs)
|
||||||
return if (left != null && left > 0) "Nog $left min" else "Nu"
|
return if (left != null && left > 0) "Nog $left min" else "Nu"
|
||||||
}
|
}
|
||||||
|
|
@ -1135,7 +1168,7 @@ private fun detailTimeWindowLabel(event: ScheduleEvent, nowMs: Long): String {
|
||||||
} else {
|
} else {
|
||||||
start.format(TimeFmt)
|
start.format(TimeFmt)
|
||||||
}
|
}
|
||||||
if (event.phase == "live" || event.isLive) return range
|
if (eventIsLive(event, nowMs)) return range
|
||||||
val today = Instant.ofEpochMilli(nowMs).atZone(ZoneNl).toLocalDate()
|
val today = Instant.ofEpochMilli(nowMs).atZone(ZoneNl).toLocalDate()
|
||||||
val day = start.toLocalDate()
|
val day = start.toLocalDate()
|
||||||
return when {
|
return when {
|
||||||
|
|
@ -1145,6 +1178,15 @@ private fun detailTimeWindowLabel(event: ScheduleEvent, nowMs: Long): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** API-flags + lokale start/stop-window (zelfde logica als master-api phaseFor). */
|
||||||
|
private fun eventIsLive(event: ScheduleEvent, nowMs: Long): Boolean {
|
||||||
|
if (event.phase.equals("live", ignoreCase = true) || event.isLive) return true
|
||||||
|
val start = parseZoned(event.startLocal, event.start) ?: return false
|
||||||
|
val stop = parseZoned(event.stopLocal, event.stop) ?: return false
|
||||||
|
val now = Instant.ofEpochMilli(nowMs).atZone(ZoneNl)
|
||||||
|
return !now.isBefore(start) && now.isBefore(stop)
|
||||||
|
}
|
||||||
|
|
||||||
private fun isStreamLabel(value: String?): Boolean {
|
private fun isStreamLabel(value: String?): Boolean {
|
||||||
val s = value?.trim().orEmpty()
|
val s = value?.trim().orEmpty()
|
||||||
if (s.isEmpty()) return false
|
if (s.isEmpty()) return false
|
||||||
|
|
|
||||||
|
|
@ -730,7 +730,9 @@ private fun MainHub(
|
||||||
prevTabName = name
|
prevTabName = name
|
||||||
}
|
}
|
||||||
|
|
||||||
val homeOverlay = tab == MainTab.Home || tab == MainTab.Events
|
// Events niet meer overlay: provider chips zitten in content met dezelfde
|
||||||
|
// verticale start als Films/Series-categoriechips onder de vaste TopNav.
|
||||||
|
val homeOverlay = tab == MainTab.Home
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
var topNavHeightPx by remember { mutableIntStateOf(0) }
|
var topNavHeightPx by remember { mutableIntStateOf(0) }
|
||||||
val topNavBlockHeight = with(density) {
|
val topNavBlockHeight = with(density) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue