diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/AppUpdateModal.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/AppUpdateModal.kt index 674872a11..fc7bccac6 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/AppUpdateModal.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/AppUpdateModal.kt @@ -117,7 +117,7 @@ fun AppUpdateModal( val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { - focusRequester.requestFocus() + runCatching { focusRequester.requestFocus() } } BackHandler { diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/ContextMenu.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/ContextMenu.kt index ee539c8c6..785c73a2a 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/ContextMenu.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/ContextMenu.kt @@ -113,7 +113,7 @@ fun ContextMenu( if (isVisible) { focusedIndex = 0 // Reset to first item if (!isMobile) { - focusRequester.requestFocus() + runCatching { focusRequester.requestFocus() } } } } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/MediaContextMenu.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/MediaContextMenu.kt index f6a2f09a4..bbf0f0bc7 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/MediaContextMenu.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/MediaContextMenu.kt @@ -124,7 +124,7 @@ fun MediaContextMenu( if (isVisible) { focusedIndex = 0 if (!isMobile) { - focusRequester.requestFocus() + runCatching { focusRequester.requestFocus() } } } } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/QuickActionMenu.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/QuickActionMenu.kt index 7c6f4fe07..0a8229eef 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/QuickActionMenu.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/QuickActionMenu.kt @@ -67,7 +67,7 @@ fun QuickActionMenu( if (isVisible) { focusedIndex = 0 ignoreNextEnter = true - focusRequester.requestFocus() + runCatching { focusRequester.requestFocus() } } } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/StreamSelector.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/StreamSelector.kt index d1d68a4c2..c94913be8 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/StreamSelector.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/StreamSelector.kt @@ -257,7 +257,7 @@ fun StreamSelector( // Request focus when visible LaunchedEffect(isVisible) { if (isVisible) { - focusRequester.requestFocus() + runCatching { focusRequester.requestFocus() } focusedIndex = 0 focusedTabIndex = 0 selectedTabIndex = 0 diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/TextInputModal.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/TextInputModal.kt index 856a24ed2..3e913ce28 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/TextInputModal.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/TextInputModal.kt @@ -118,7 +118,7 @@ fun TextInputModal( // Request focus on input when modal becomes visible and show keyboard LaunchedEffect(isVisible) { if (isVisible) { - inputFocusRequester.requestFocus() + runCatching { inputFocusRequester.requestFocus() } focusedButton = -1 // Delay to ensure focus is set before showing keyboard kotlinx.coroutines.delay(200) @@ -160,7 +160,7 @@ fun TextInputModal( Key.DirectionUp -> { if (focusedButton >= 0) { focusedButton = -1 - inputFocusRequester.requestFocus() + runCatching { inputFocusRequester.requestFocus() } showKeyboard() } true diff --git a/app/src/main/kotlin/com/arflix/tv/ui/focus/ArvioDpadFocus.kt b/app/src/main/kotlin/com/arflix/tv/ui/focus/ArvioDpadFocus.kt index 977d07a37..d53b8a542 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/focus/ArvioDpadFocus.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/focus/ArvioDpadFocus.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.geometry.Rect import androidx.compose.ui.input.key.Key @@ -18,14 +17,20 @@ import kotlin.Unit @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) fun Modifier.arvioDpadFocusGroup( - restoreFocusRequester: FocusRequester? = null, enableFocusRestorer: Boolean = true ): Modifier { - val restorer = when { - !enableFocusRestorer -> Modifier - restoreFocusRequester != null -> Modifier.focusRestorer { restoreFocusRequester } - else -> Modifier.focusRestorer() - } + // Compose's focusRestorer(onRestoreFailed = { restoreFocusRequester }) captures the + // requester at modifier-application time and, on a focus enter whose automatic restore + // fails, calls requestFocus() ITSELF — deep inside the modifier, without a try/catch. + // When the restore target lives in a LazyColumn row that is not yet composed/attached + // (the case right after categories finish loading on a fresh TV entry), that internal + // requestFocus() throws IllegalStateException: FocusRequester is not initialized and + // crashes the app, unreachable by the runCatching guards wrapping direct calls. + // + // Keep focus restoration limited to Compose's own saved focus history. Callers that + // need an initial focus target should request it after the target is attached instead + // of supplying a fallback that Compose can invoke before a lazy item is composed. + val restorer = if (enableFocusRestorer) Modifier.focusRestorer() else Modifier return this.then(restorer).focusGroup() } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt index a9093ce9a..60b56799c 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt @@ -312,12 +312,12 @@ fun SearchScreen( try { filtersFocusRequester.requestFocus() } catch (_: Exception) {} } else { focusZone = FocusZone.SEARCH_INPUT - searchFocusRequester.requestFocus() + runCatching { searchFocusRequester.requestFocus() } } } FocusZone.FILTERS -> { focusZone = FocusZone.SEARCH_INPUT - searchFocusRequester.requestFocus() + runCatching { searchFocusRequester.requestFocus() } } FocusZone.SEARCH_INPUT -> { focusZone = FocusZone.SIDEBAR diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/CategorySidebar.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/CategorySidebar.kt index c73432b91..d598650eb 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/CategorySidebar.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/CategorySidebar.kt @@ -260,9 +260,7 @@ fun CategorySidebar( // hands it the selector on entry and again every time the lazy list // recomposes underneath the focused row — which is what pinned the // selector in the search box while the playlist loaded. - .arvioDpadFocusGroup( - restoreFocusRequester = if (categoriesLoaded) selectedCategoryFocusRequester else null, - ) + .arvioDpadFocusGroup() .onFocusChanged { focusState -> if (focusState.hasFocus) { onFocusEnter()