fix Events focus oscillating between hero and card on back
This commit is contained in:
parent
0099f9079c
commit
d549609dbe
3 changed files with 75 additions and 27 deletions
|
|
@ -20,8 +20,8 @@ android {
|
|||
applicationId = "nl.vonas.mediacluster.tv"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 122
|
||||
versionName = "0.14.59"
|
||||
versionCode = 123
|
||||
versionName = "0.14.60"
|
||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ import androidx.tv.material3.Text
|
|||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import nl.vonas.mediacluster.tv.data.ApiClient
|
||||
import nl.vonas.mediacluster.tv.data.ScheduleEvent
|
||||
import nl.vonas.mediacluster.tv.ui.components.HomeChrome
|
||||
|
|
@ -133,6 +134,8 @@ fun EventsScreen(
|
|||
var restoreCardReady by remember { mutableStateOf(false) }
|
||||
var heroCtaFocused by remember { mutableStateOf(false) }
|
||||
var placeholderHadFocus by remember { mutableStateOf(false) }
|
||||
// Houd carousel stil tijdens + kort na restore zodat Crossfade geen focus steelt.
|
||||
var pauseCarouselAfterRestore by remember { mutableStateOf(false) }
|
||||
val listState = rememberLazyListState()
|
||||
val chipsFocus = remember { FocusRequester() }
|
||||
val firstRailFocus = remember { FocusRequester() }
|
||||
|
|
@ -335,7 +338,8 @@ fun EventsScreen(
|
|||
else -> railFocusReady
|
||||
}
|
||||
|
||||
// Detail → back: card-restore als we vanaf een rail openden; anders hero CTA (veilig).
|
||||
// Detail → back: één restore-owner. Card eerst; hero-fallback alleen als card
|
||||
// nog niet gelukt is (timeout-job wordt bij succes gecanceld).
|
||||
LaunchedEffect(
|
||||
pendingFocusRestore,
|
||||
focusRestoreNonce,
|
||||
|
|
@ -345,53 +349,89 @@ fun EventsScreen(
|
|||
restoreIsFirstOfFirstRail,
|
||||
) {
|
||||
if (!pendingFocusRestore || !hasContent) return@LaunchedEffect
|
||||
pauseCarouselAfterRestore = true
|
||||
delay(60)
|
||||
onContentFocused()
|
||||
|
||||
var focusedCard = false
|
||||
if (canRestoreCard && restoreListIndex >= 0 && activeRestoreFocus != null) {
|
||||
runCatching {
|
||||
listState.scrollToItem(restoreListIndex)
|
||||
delay(50)
|
||||
val ready = if (restoreIsFirstOfFirstRail) {
|
||||
awaitReady({ railFocusReady }, timeoutMs = 500)
|
||||
} else {
|
||||
awaitReady({ restoreCardReady }, timeoutMs = 500)
|
||||
}
|
||||
if (ready) {
|
||||
activeRestoreFocus.requestFocus()
|
||||
focusedCard = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var restoredSuccessfully = false
|
||||
|
||||
if (!focusedCard) {
|
||||
// Hero/entry fallback — scroll naar top alleen als we géén card restoren.
|
||||
suspend fun focusHeroFallback() {
|
||||
if (restoredSuccessfully) return
|
||||
val entryOk = awaitReady({
|
||||
when {
|
||||
showHero -> heroFocusReady
|
||||
showProviderChips -> chipsFocusReady
|
||||
else -> railFocusReady
|
||||
}
|
||||
}, timeoutMs = 500)
|
||||
}, timeoutMs = 400)
|
||||
if (restoredSuccessfully) return
|
||||
if (entryOk) {
|
||||
runCatching { listState.scrollToItem(0) }
|
||||
delay(40)
|
||||
if (restoredSuccessfully) return
|
||||
runCatching { contentFocusRequester.requestFocus() }
|
||||
}
|
||||
}
|
||||
onFocusRestoreConsumed()
|
||||
|
||||
// Timeout → hero alleen als card-restore nog niet slaagde.
|
||||
val heroTimeoutJob = launch {
|
||||
delay(550)
|
||||
if (!restoredSuccessfully) {
|
||||
focusHeroFallback()
|
||||
if (!restoredSuccessfully) onFocusRestoreConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
if (canRestoreCard && restoreListIndex >= 0 && activeRestoreFocus != null) {
|
||||
runCatching {
|
||||
listState.scrollToItem(restoreListIndex)
|
||||
delay(50)
|
||||
yield()
|
||||
val ready = if (restoreIsFirstOfFirstRail) {
|
||||
awaitReady({ railFocusReady }, timeoutMs = 450)
|
||||
} else {
|
||||
awaitReady({ restoreCardReady }, timeoutMs = 450)
|
||||
}
|
||||
if (ready) {
|
||||
activeRestoreFocus.requestFocus()
|
||||
restoredSuccessfully = true
|
||||
heroTimeoutJob.cancel()
|
||||
// Clear pending IMMEDIATELY zodat late effects niets meer doen.
|
||||
onFocusRestoreConsumed()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Geen card-target (hero-open of onbekend id): meteen hero, cancel timeout.
|
||||
heroTimeoutJob.cancel()
|
||||
focusHeroFallback()
|
||||
onFocusRestoreConsumed()
|
||||
restoredSuccessfully = true
|
||||
}
|
||||
|
||||
if (!restoredSuccessfully) {
|
||||
// Card-poging faalde vóór timeout — wacht op fallback-job.
|
||||
if (heroTimeoutJob.isActive) heroTimeoutJob.join()
|
||||
else {
|
||||
focusHeroFallback()
|
||||
onFocusRestoreConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
// Carousel nog ~2s stil na restore (Crossfade mag focus niet stelen).
|
||||
delay(2_000)
|
||||
pauseCarouselAfterRestore = false
|
||||
}
|
||||
|
||||
LaunchedEffect(entryReady, hasContent) {
|
||||
LaunchedEffect(entryReady, hasContent, pendingFocusRestore) {
|
||||
if (!entryReady || !hasContent || !placeholderHadFocus) return@LaunchedEffect
|
||||
if (pendingFocusRestore) return@LaunchedEffect
|
||||
delay(40)
|
||||
onContentFocused()
|
||||
runCatching { contentFocusRequester.requestFocus() }
|
||||
placeholderHadFocus = false
|
||||
}
|
||||
|
||||
val pauseCarousel = pendingFocusRestore || heroCtaFocused
|
||||
val pauseCarousel = pendingFocusRestore || pauseCarouselAfterRestore || heroCtaFocused
|
||||
// NOOIT dual-bind: placeholder alleen als entry-target NIET in de tree hangt.
|
||||
val entryInTree = hasContent && (showHero || showProviderChips || hasRails)
|
||||
|
||||
|
|
@ -447,13 +487,18 @@ fun EventsScreen(
|
|||
onFocusReady = { heroFocusReady = it },
|
||||
onFocus = {
|
||||
onContentFocused()
|
||||
expandHeroFully()
|
||||
// Niet tijdens card-restore: scroll/BringIntoView trekt focus terug naar hero.
|
||||
if (!pendingFocusRestore && !pauseCarouselAfterRestore) {
|
||||
expandHeroFully()
|
||||
}
|
||||
},
|
||||
onCtaFocusChanged = { focused ->
|
||||
heroCtaFocused = focused
|
||||
if (focused) {
|
||||
onContentFocused()
|
||||
expandHeroFully()
|
||||
if (!pendingFocusRestore && !pauseCarouselAfterRestore) {
|
||||
expandHeroFully()
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpen = { id ->
|
||||
|
|
|
|||
|
|
@ -720,9 +720,12 @@ 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.
|
||||
// Bij pending restore is EventsScreen de enige owner (card of hero-fallback) —
|
||||
// géén parallel hero requestFocus (race: hero→card→hero).
|
||||
LaunchedEffect(eventsReturnNonce) {
|
||||
if (eventsReturnNonce <= 0 || tab != MainTab.Events) return@LaunchedEffect
|
||||
navFocused = false
|
||||
if (pendingEventsFocusRestore) return@LaunchedEffect
|
||||
delay(100)
|
||||
runCatching { eventsContentFocus.requestFocus() }
|
||||
delay(180)
|
||||
|
|
|
|||
Loading…
Reference in a new issue