Fix Android TV Events focus hang after tab switch and detail back.
Simplify Events focus to the Home hero pattern: no dual-bound FocusRequester, hero outside LazyColumn, and stable CTA restore so D-pad stays usable.
This commit is contained in:
parent
cbacf1af41
commit
3c356001b2
3 changed files with 93 additions and 146 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 = 117
|
versionCode = 118
|
||||||
versionName = "0.14.54"
|
versionName = "0.14.55"
|
||||||
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")
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,9 @@ fun EventsScreen(
|
||||||
var nowMs by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
var nowMs by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||||
var heroFocusReady by remember { mutableStateOf(false) }
|
var heroFocusReady by remember { mutableStateOf(false) }
|
||||||
var railFocusReady by remember { mutableStateOf(false) }
|
var railFocusReady by remember { mutableStateOf(false) }
|
||||||
|
var heroCtaFocused by remember { mutableStateOf(false) }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val firstRailFocus = remember { FocusRequester() }
|
val firstRailFocus = remember { FocusRequester() }
|
||||||
val restoreCardFocus = remember { FocusRequester() }
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
suspend fun refresh() {
|
suspend fun refresh() {
|
||||||
|
|
@ -153,31 +153,16 @@ fun EventsScreen(
|
||||||
if (upcoming.isEmpty()) null else label to upcoming
|
if (upcoming.isEmpty()) null else label to upcoming
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val restoreInLive = remember(live, restoreEventId) {
|
val hasHeroContent = filtered.isNotEmpty()
|
||||||
!restoreEventId.isNullOrBlank() && live.any { it.id == restoreEventId }
|
val heroItems = liveHeroItems.ifEmpty { listOfNotNull(filtered.firstOrNull()) }
|
||||||
}
|
val heroHeight = 480.dp
|
||||||
val restoreInUpcoming = remember(upcomingRails, restoreEventId) {
|
// Rails starten onder de hero met lichte overlap (Home-achtig).
|
||||||
!restoreEventId.isNullOrBlank() &&
|
val railStart = 420.dp
|
||||||
upcomingRails.any { (_, day) -> day.any { it.id == restoreEventId } }
|
|
||||||
}
|
|
||||||
val canRestoreCard = restoreInLive || restoreInUpcoming
|
|
||||||
|
|
||||||
val firstRailListIndex = remember(providers, live, upcomingRails) {
|
val firstRailListIndex = remember(providers, live, upcomingRails) {
|
||||||
var idx = 1 // after hero
|
var idx = 1 // after spacer
|
||||||
if (providers.size > 1) idx++
|
if (providers.size > 1) idx++
|
||||||
if (live.isNotEmpty() || upcomingRails.isNotEmpty()) idx else -1
|
if (live.isNotEmpty() || upcomingRails.isNotEmpty()) idx else -1
|
||||||
}
|
}
|
||||||
val restoreListIndex = remember(providers, live, upcomingRails, restoreEventId, restoreInLive) {
|
|
||||||
if (restoreEventId.isNullOrBlank()) return@remember -1
|
|
||||||
var idx = 1
|
|
||||||
if (providers.size > 1) idx++
|
|
||||||
if (restoreInLive) return@remember idx
|
|
||||||
if (live.isNotEmpty()) idx++
|
|
||||||
upcomingRails.forEachIndexed { i, (_, day) ->
|
|
||||||
if (day.any { it.id == restoreEventId }) return@remember idx + i
|
|
||||||
}
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun awaitRailReady(timeoutMs: Long = 900): Boolean {
|
suspend fun awaitRailReady(timeoutMs: Long = 900): Boolean {
|
||||||
val deadline = System.currentTimeMillis() + timeoutMs
|
val deadline = System.currentTimeMillis() + timeoutMs
|
||||||
|
|
@ -200,56 +185,38 @@ fun EventsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Na terugkeer van detail/player: focus één keer voorspelbaar herstellen.
|
// Stabiele restore: altijd hero CTA — geen card-FocusRequester races.
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
pendingFocusRestore,
|
pendingFocusRestore,
|
||||||
focusRestoreNonce,
|
focusRestoreNonce,
|
||||||
filtered.isNotEmpty(),
|
hasHeroContent,
|
||||||
heroFocusReady,
|
heroFocusReady,
|
||||||
railFocusReady,
|
|
||||||
canRestoreCard,
|
|
||||||
) {
|
) {
|
||||||
if (!pendingFocusRestore || filtered.isEmpty() || !heroFocusReady) return@LaunchedEffect
|
if (!pendingFocusRestore || !hasHeroContent || !heroFocusReady) return@LaunchedEffect
|
||||||
if (canRestoreCard && !railFocusReady) return@LaunchedEffect
|
delay(60)
|
||||||
delay(80)
|
|
||||||
onContentFocused()
|
onContentFocused()
|
||||||
val scrolled = listState.firstVisibleItemIndex > 0 ||
|
|
||||||
listState.firstVisibleItemScrollOffset > 40
|
|
||||||
if (scrolled) {
|
|
||||||
runCatching { listState.scrollToItem(0) }
|
runCatching { listState.scrollToItem(0) }
|
||||||
delay(40)
|
delay(40)
|
||||||
}
|
|
||||||
var focused = false
|
|
||||||
if (canRestoreCard) {
|
|
||||||
focused = runCatching {
|
|
||||||
val scrollTo = restoreListIndex.takeIf { it >= 0 } ?: firstRailListIndex
|
|
||||||
if (scrollTo >= 0) {
|
|
||||||
listState.animateScrollToItem(scrollTo)
|
|
||||||
delay(50)
|
|
||||||
}
|
|
||||||
restoreCardFocus.requestFocus()
|
|
||||||
}.isSuccess
|
|
||||||
}
|
|
||||||
if (!focused) {
|
|
||||||
delay(40)
|
|
||||||
runCatching { contentFocusRequester.requestFocus() }
|
runCatching { contentFocusRequester.requestFocus() }
|
||||||
}
|
|
||||||
onFocusRestoreConsumed()
|
onFocusRestoreConsumed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder→hero: als TopNav-down op de 1dp-box landde vóór de CTA klaar was.
|
// Placeholder had focus tijdens laden → doorzetten naar hero CTA zodra klaar.
|
||||||
var placeholderHadFocus by remember { mutableStateOf(false) }
|
var placeholderHadFocus by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(heroFocusReady) {
|
LaunchedEffect(heroFocusReady, hasHeroContent) {
|
||||||
if (!heroFocusReady || !placeholderHadFocus) return@LaunchedEffect
|
if (!heroFocusReady || !hasHeroContent || !placeholderHadFocus) return@LaunchedEffect
|
||||||
delay(40)
|
delay(40)
|
||||||
onContentFocused()
|
onContentFocused()
|
||||||
runCatching { contentFocusRequester.requestFocus() }
|
runCatching { contentFocusRequester.requestFocus() }
|
||||||
placeholderHadFocus = false
|
placeholderHadFocus = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val pauseCarousel = pendingFocusRestore || heroCtaFocused
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize().background(McColors.Bg)) {
|
Box(Modifier.fillMaxSize().background(McColors.Bg)) {
|
||||||
// TopNav "down" moet altijd een geldig doel hebben (ook tijdens laden).
|
// NOOIT dual-bind: placeholder alleen als hero NIET in de tree hangt.
|
||||||
if (!heroFocusReady) {
|
// (Home had dezelfde race: placeholder + CTA tegelijk → Shield hang.)
|
||||||
|
if (!hasHeroContent) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(1.dp)
|
.size(1.dp)
|
||||||
|
|
@ -273,34 +240,44 @@ fun EventsScreen(
|
||||||
Text(error!!, color = McColors.Danger, fontSize = 16.sp)
|
Text(error!!, color = McColors.Danger, fontSize = 16.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filtered.isEmpty() -> {
|
!hasHeroContent -> {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text("Geen events in de komende dagen", color = McColors.TextDim, fontSize = 18.sp)
|
Text("Geen events in de komende dagen", color = McColors.TextDim, fontSize = 18.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
// Hero BUITEN LazyColumn — zelfde stabiele patroon als Home SpotlightHero.
|
||||||
state = listState,
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
Modifier
|
||||||
contentPadding = PaddingValues(bottom = 48.dp),
|
.align(Alignment.TopCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(heroHeight),
|
||||||
) {
|
) {
|
||||||
item(key = "hero") {
|
|
||||||
if (liveHeroItems.isNotEmpty() || filtered.isNotEmpty()) {
|
|
||||||
EventHero(
|
EventHero(
|
||||||
events = liveHeroItems.ifEmpty {
|
events = heroItems,
|
||||||
listOfNotNull(filtered.firstOrNull())
|
|
||||||
},
|
|
||||||
nowMs = nowMs,
|
nowMs = nowMs,
|
||||||
focusRequester = contentFocusRequester,
|
focusRequester = contentFocusRequester,
|
||||||
navFocusRequester = navFocusRequester,
|
navFocusRequester = navFocusRequester,
|
||||||
railFocusRequester = firstRailFocus.takeIf { railFocusReady },
|
railFocusRequester = firstRailFocus.takeIf { railFocusReady },
|
||||||
|
pauseCarousel = pauseCarousel,
|
||||||
onFocusReady = { heroFocusReady = it },
|
onFocusReady = { heroFocusReady = it },
|
||||||
onFocus = onContentFocused,
|
onFocus = onContentFocused,
|
||||||
|
onCtaFocusChanged = { focused ->
|
||||||
|
heroCtaFocused = focused
|
||||||
|
if (focused) onContentFocused()
|
||||||
|
},
|
||||||
onOpen = { id -> onOpenEvent(id) },
|
onOpen = { id -> onOpenEvent(id) },
|
||||||
onPlay = { id -> onPlayEvent(id) },
|
onPlay = { id -> onPlayEvent(id) },
|
||||||
onRequestRailFocus = { focusFirstRail() },
|
onRequestRailFocus = { focusFirstRail() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(bottom = 48.dp),
|
||||||
|
) {
|
||||||
|
item(key = "hero-spacer") {
|
||||||
|
Spacer(Modifier.height(railStart))
|
||||||
}
|
}
|
||||||
if (providers.size > 1) {
|
if (providers.size > 1) {
|
||||||
item(key = "providers") {
|
item(key = "providers") {
|
||||||
|
|
@ -318,12 +295,8 @@ fun EventsScreen(
|
||||||
events = live,
|
events = live,
|
||||||
nowMs = nowMs,
|
nowMs = nowMs,
|
||||||
onOpen = onOpenEvent,
|
onOpen = onOpenEvent,
|
||||||
onFocus = {
|
onFocus = onContentFocused,
|
||||||
onContentFocused()
|
|
||||||
},
|
|
||||||
firstItemFocusRequester = firstRailFocus.takeIf { live.isNotEmpty() },
|
firstItemFocusRequester = firstRailFocus.takeIf { live.isNotEmpty() },
|
||||||
restoreEventId = restoreEventId.takeIf { restoreInLive },
|
|
||||||
restoreFocusRequester = restoreCardFocus.takeIf { restoreInLive },
|
|
||||||
focusUpTo = contentFocusRequester.takeIf { heroFocusReady },
|
focusUpTo = contentFocusRequester.takeIf { heroFocusReady },
|
||||||
onFirstFocusReady = { ready ->
|
onFirstFocusReady = { ready ->
|
||||||
if (live.isNotEmpty()) railFocusReady = ready
|
if (live.isNotEmpty()) railFocusReady = ready
|
||||||
|
|
@ -333,9 +306,6 @@ fun EventsScreen(
|
||||||
}
|
}
|
||||||
upcomingRails.forEachIndexed { index, (label, dayEvents) ->
|
upcomingRails.forEachIndexed { index, (label, dayEvents) ->
|
||||||
val isFirstUpcoming = live.isEmpty() && index == 0
|
val isFirstUpcoming = live.isEmpty() && index == 0
|
||||||
val railHasRestore = !restoreInLive &&
|
|
||||||
restoreEventId != null &&
|
|
||||||
dayEvents.any { it.id == restoreEventId }
|
|
||||||
item(key = "day-$label") {
|
item(key = "day-$label") {
|
||||||
EventRail(
|
EventRail(
|
||||||
title = label,
|
title = label,
|
||||||
|
|
@ -344,8 +314,6 @@ fun EventsScreen(
|
||||||
onOpen = onOpenEvent,
|
onOpen = onOpenEvent,
|
||||||
onFocus = onContentFocused,
|
onFocus = onContentFocused,
|
||||||
firstItemFocusRequester = firstRailFocus.takeIf { isFirstUpcoming },
|
firstItemFocusRequester = firstRailFocus.takeIf { isFirstUpcoming },
|
||||||
restoreEventId = restoreEventId.takeIf { railHasRestore },
|
|
||||||
restoreFocusRequester = restoreCardFocus.takeIf { railHasRestore },
|
|
||||||
focusUpTo = contentFocusRequester.takeIf { heroFocusReady && isFirstUpcoming },
|
focusUpTo = contentFocusRequester.takeIf { heroFocusReady && isFirstUpcoming },
|
||||||
onFirstFocusReady = { ready ->
|
onFirstFocusReady = { ready ->
|
||||||
if (isFirstUpcoming) railFocusReady = ready
|
if (isFirstUpcoming) railFocusReady = ready
|
||||||
|
|
@ -707,8 +675,10 @@ private fun EventHero(
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navFocusRequester: FocusRequester,
|
navFocusRequester: FocusRequester,
|
||||||
railFocusRequester: FocusRequester?,
|
railFocusRequester: FocusRequester?,
|
||||||
|
pauseCarousel: Boolean = false,
|
||||||
onFocusReady: (Boolean) -> Unit,
|
onFocusReady: (Boolean) -> Unit,
|
||||||
onFocus: () -> Unit,
|
onFocus: () -> Unit,
|
||||||
|
onCtaFocusChanged: (Boolean) -> Unit = {},
|
||||||
onOpen: (String) -> Unit,
|
onOpen: (String) -> Unit,
|
||||||
onPlay: (String) -> Unit,
|
onPlay: (String) -> Unit,
|
||||||
onRequestRailFocus: () -> Unit = {},
|
onRequestRailFocus: () -> Unit = {},
|
||||||
|
|
@ -717,8 +687,8 @@ private fun EventHero(
|
||||||
var heroIndex by remember { mutableIntStateOf(0) }
|
var heroIndex by remember { mutableIntStateOf(0) }
|
||||||
val idsKey = remember(events) { events.joinToString("|") { it.id } }
|
val idsKey = remember(events) { events.joinToString("|") { it.id } }
|
||||||
|
|
||||||
LaunchedEffect(idsKey) {
|
LaunchedEffect(idsKey, pauseCarousel) {
|
||||||
heroIndex = 0
|
if (pauseCarousel) return@LaunchedEffect
|
||||||
if (events.size <= 1) return@LaunchedEffect
|
if (events.size <= 1) return@LaunchedEffect
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(HeroRotateMs)
|
delay(HeroRotateMs)
|
||||||
|
|
@ -819,27 +789,30 @@ private fun EventHero(
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
// CTAs buiten Crossfade → FocusRequester blijft stabiel tijdens rotate
|
// Eén stabiele primary CTA (geen playable if/else remount) → FocusRequester blijft bound.
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
DisposableEffect(focusRequester) {
|
DisposableEffect(focusRequester) {
|
||||||
onFocusReady(true)
|
onFocusReady(true)
|
||||||
onDispose { onFocusReady(false) }
|
onDispose { onFocusReady(false) }
|
||||||
}
|
}
|
||||||
|
McDetailButton(
|
||||||
|
if (current.playable) "Kijk live" else ctaLabel(current, nowMs),
|
||||||
|
primary = true,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
focusUp = navFocusRequester,
|
||||||
|
focusDown = railFocusRequester,
|
||||||
|
onFocus = onFocus,
|
||||||
|
onFocusChanged = onCtaFocusChanged,
|
||||||
|
onFocusDownFallback = if (railFocusRequester == null) {
|
||||||
|
({ onRequestRailFocus() })
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (current.playable) onPlay(current.id) else onOpen(current.id)
|
||||||
|
},
|
||||||
|
)
|
||||||
if (current.playable) {
|
if (current.playable) {
|
||||||
McDetailButton(
|
|
||||||
"Kijk live",
|
|
||||||
primary = true,
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
focusUp = navFocusRequester,
|
|
||||||
focusDown = railFocusRequester,
|
|
||||||
onFocus = onFocus,
|
|
||||||
onFocusDownFallback = if (railFocusRequester == null) {
|
|
||||||
({ onRequestRailFocus() })
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
onClick = { onPlay(current.id) },
|
|
||||||
)
|
|
||||||
McDetailButton(
|
McDetailButton(
|
||||||
"Meer info",
|
"Meer info",
|
||||||
focusUp = navFocusRequester,
|
focusUp = navFocusRequester,
|
||||||
|
|
@ -852,21 +825,6 @@ private fun EventHero(
|
||||||
},
|
},
|
||||||
onClick = { onOpen(current.id) },
|
onClick = { onOpen(current.id) },
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
McDetailButton(
|
|
||||||
ctaLabel(current, nowMs),
|
|
||||||
primary = true,
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
focusUp = navFocusRequester,
|
|
||||||
focusDown = railFocusRequester,
|
|
||||||
onFocus = onFocus,
|
|
||||||
onFocusDownFallback = if (railFocusRequester == null) {
|
|
||||||
({ onRequestRailFocus() })
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
onClick = { onOpen(current.id) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -920,22 +878,10 @@ private fun EventRail(
|
||||||
onOpen: (String) -> Unit,
|
onOpen: (String) -> Unit,
|
||||||
onFocus: () -> Unit,
|
onFocus: () -> Unit,
|
||||||
firstItemFocusRequester: FocusRequester? = null,
|
firstItemFocusRequester: FocusRequester? = null,
|
||||||
restoreEventId: String? = null,
|
|
||||||
restoreFocusRequester: FocusRequester? = null,
|
|
||||||
focusUpTo: FocusRequester? = null,
|
focusUpTo: FocusRequester? = null,
|
||||||
onFirstFocusReady: ((Boolean) -> Unit)? = null,
|
onFirstFocusReady: ((Boolean) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
if (events.isEmpty()) return
|
if (events.isEmpty()) return
|
||||||
val rowState = rememberLazyListState()
|
|
||||||
val restoreIndex = remember(events, restoreEventId) {
|
|
||||||
if (restoreEventId.isNullOrBlank()) -1
|
|
||||||
else events.indexOfFirst { it.id == restoreEventId }
|
|
||||||
}
|
|
||||||
LaunchedEffect(restoreIndex) {
|
|
||||||
if (restoreIndex > 0) {
|
|
||||||
runCatching { rowState.scrollToItem(restoreIndex) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(Modifier.padding(top = 20.dp)) {
|
Column(Modifier.padding(top = 20.dp)) {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
|
|
@ -946,37 +892,24 @@ private fun EventRail(
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = rowState,
|
|
||||||
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset),
|
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset),
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
) {
|
) {
|
||||||
items(events, key = { it.id }) { ev ->
|
items(events, key = { it.id }) { ev ->
|
||||||
val isFirst = ev.id == events.first().id
|
val isFirst = ev.id == events.first().id
|
||||||
val isRestore = restoreEventId != null && ev.id == restoreEventId
|
|
||||||
if (isFirst && firstItemFocusRequester != null) {
|
if (isFirst && firstItemFocusRequester != null) {
|
||||||
DisposableEffect(firstItemFocusRequester) {
|
DisposableEffect(firstItemFocusRequester) {
|
||||||
onFirstFocusReady?.invoke(true)
|
onFirstFocusReady?.invoke(true)
|
||||||
onDispose { onFirstFocusReady?.invoke(false) }
|
onDispose { onFirstFocusReady?.invoke(false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val cardRequester = when {
|
|
||||||
isRestore && restoreFocusRequester != null -> restoreFocusRequester
|
|
||||||
isFirst -> firstItemFocusRequester
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
EventCard(
|
EventCard(
|
||||||
event = ev,
|
event = ev,
|
||||||
nowMs = nowMs,
|
nowMs = nowMs,
|
||||||
onOpen = { onOpen(ev.id) },
|
onOpen = { onOpen(ev.id) },
|
||||||
onFocus = onFocus,
|
onFocus = onFocus,
|
||||||
focusRequester = cardRequester,
|
focusRequester = firstItemFocusRequester.takeIf { isFirst },
|
||||||
extraFocusRequester = when {
|
focusUp = focusUpTo.takeIf { isFirst },
|
||||||
isRestore && isFirst && firstItemFocusRequester != null &&
|
|
||||||
restoreFocusRequester != null &&
|
|
||||||
firstItemFocusRequester !== restoreFocusRequester -> firstItemFocusRequester
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
focusUp = focusUpTo.takeIf { isFirst || isRestore },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -991,7 +924,6 @@ private fun EventCard(
|
||||||
onOpen: () -> Unit,
|
onOpen: () -> Unit,
|
||||||
onFocus: () -> Unit,
|
onFocus: () -> Unit,
|
||||||
focusRequester: FocusRequester? = null,
|
focusRequester: FocusRequester? = null,
|
||||||
extraFocusRequester: FocusRequester? = null,
|
|
||||||
focusUp: FocusRequester? = null,
|
focusUp: FocusRequester? = null,
|
||||||
) {
|
) {
|
||||||
var focused by remember { mutableStateOf(false) }
|
var focused by remember { mutableStateOf(false) }
|
||||||
|
|
@ -1000,7 +932,6 @@ private fun EventCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(280.dp)
|
.width(280.dp)
|
||||||
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
|
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
|
||||||
.then(if (extraFocusRequester != null) Modifier.focusRequester(extraFocusRequester) else Modifier)
|
|
||||||
.then(
|
.then(
|
||||||
if (focusUp != null) {
|
if (focusUp != null) {
|
||||||
Modifier.focusProperties { up = focusUp }
|
Modifier.focusProperties { up = focusUp }
|
||||||
|
|
@ -1166,6 +1097,7 @@ private fun McDetailButton(
|
||||||
focusUp: FocusRequester? = null,
|
focusUp: FocusRequester? = null,
|
||||||
focusDown: FocusRequester? = null,
|
focusDown: FocusRequester? = null,
|
||||||
onFocus: (() -> Unit)? = null,
|
onFocus: (() -> Unit)? = null,
|
||||||
|
onFocusChanged: ((Boolean) -> Unit)? = null,
|
||||||
/** When gallery/rail is off-screen, scroll+focus before default D-pad move. */
|
/** When gallery/rail is off-screen, scroll+focus before default D-pad move. */
|
||||||
onFocusDownFallback: (() -> Unit)? = null,
|
onFocusDownFallback: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
|
@ -1198,6 +1130,7 @@ private fun McDetailButton(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
|
onFocusChanged?.invoke(it.isFocused)
|
||||||
if (it.isFocused) onFocus?.invoke()
|
if (it.isFocused) onFocus?.invoke()
|
||||||
}
|
}
|
||||||
.heightIn(min = 36.dp, max = 40.dp)
|
.heightIn(min = 36.dp, max = 40.dp)
|
||||||
|
|
|
||||||
|
|
@ -713,6 +713,17 @@ private fun MainHub(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Terug van EventDetail/Player: MainHub is opnieuw gemount — forceer content-focus
|
||||||
|
// zodat TopNav niet “nav-only” blijft en D-pad down niet doodloopt.
|
||||||
|
LaunchedEffect(eventsReturnNonce) {
|
||||||
|
if (eventsReturnNonce <= 0 || tab != MainTab.Events) return@LaunchedEffect
|
||||||
|
navFocused = false
|
||||||
|
delay(100)
|
||||||
|
runCatching { eventsContentFocus.requestFocus() }
|
||||||
|
delay(180)
|
||||||
|
runCatching { eventsContentFocus.requestFocus() }
|
||||||
|
}
|
||||||
|
|
||||||
// Reset scroll alleen bij echte tab-wissel naar Home (niet bij terugkeer uit detail/player)
|
// Reset scroll alleen bij echte tab-wissel naar Home (niet bij terugkeer uit detail/player)
|
||||||
LaunchedEffect(tab) {
|
LaunchedEffect(tab) {
|
||||||
val name = tab.name
|
val name = tab.name
|
||||||
|
|
@ -772,6 +783,9 @@ private fun MainHub(
|
||||||
navFocused = false
|
navFocused = false
|
||||||
onTvFocusContent()
|
onTvFocusContent()
|
||||||
hubScope.launch {
|
hubScope.launch {
|
||||||
|
// Wacht tot EventsScreen placeholder of hero CTA de requester bindt.
|
||||||
|
delay(160)
|
||||||
|
runCatching { eventsContentFocus.requestFocus() }
|
||||||
delay(120)
|
delay(120)
|
||||||
runCatching { eventsContentFocus.requestFocus() }
|
runCatching { eventsContentFocus.requestFocus() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue