# Phase 1 Critical Fixes — 2026-05-20

Session that fixed 5 functional blockers in the NexStream Android TV app: Debrid settings focus, Home catalog empty-state, Profile creation focus trap, Addon discover unresponsiveness, and Trakt Connect no-op. All fixes compiled with 0 errors.

---

## Fix A: Debrid Settings — D-pad Focus Navigation

**File:** `ui/screens/settings/SettingsComponents.kt`
**Symptom:** In Settings → Debrid, D-pad down-arrow skips from the top provider card directly to the bottom button, skipping all intermediate fields.

**Root cause:** `SettingsScaffold` wrapped the entire content in a single `LazyColumn` item:
```kotlin
LazyColumn {
    item(key = "content") {
        Column {  // all settings fields inside one lazy slot
            // 10+ focusable elements
        }
    }
}
```

`LazyColumn`'s focus tracking operates at the item level. A single item containing many focusables loses granularity — the TV framework sees one big focusable blob.

**Fix:**
```kotlin
Column(
    modifier = modifier.verticalScroll(rememberScrollState()),
    horizontalAlignment = Alignment.CenterHorizontally,
) {
    // all content directly — each focusable is a peer in the Column
}
```

**Build verification:** `./gradlew :app:compileDebugKotlin` — 0 errors.

---

## Fix B: Home Catalogs — Empty State Guidance

**Files:** `ui/screens/home/HomeViewModel.kt`, `ui/screens/home/HomeScreen.kt`
**Symptom:** Home screen is completely blank when no Stremio catalog addons are installed. No error, no guidance.

**Root cause:** `HomeViewModel.fetchCatalogSafe()` swallowed all exceptions:
```kotlin
private suspend fun fetchCatalogSafe(addon: CatalogAddon): List<MediaRow> {
    return runCatching { addon.getRows() }.getOrElse { emptyList() }
}
```

**Fix — explicit state propagation:**
```kotlin
// ViewModel
private val _catalogsUnavailable = MutableStateFlow(false)
val catalogsUnavailable: StateFlow<Boolean> = _catalogsUnavailable.asStateFlow()

suspend fun loadHomeData() {
    val catalogAddons = addonRepository.getCatalogAddons()
    if (catalogAddons.isEmpty()) {
        _catalogsUnavailable.value = true
        return
    }
    _catalogsUnavailable.value = false
    // ... load rows ...
}
```

```kotlin
// Screen — banner with navigation action
if (uiState.catalogsUnavailable) {
    Surface(
        colors = SurfaceDefaults.colors(containerColor = Color(0xFF332200)),
        modifier = Modifier.padding(16.dp),
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "No catalog addons installed.",
                color = Color(0xFFFFCC00),
                fontWeight = FontWeight.Bold,
            )
            Text(
                text = "Go to Addons → Discover to install a catalog provider (e.g., Cinema Meta or Streaming Catalogs).",
                color = Color(0xFFFFCC00),
            )
            Button(
                onClick = { navController.navigate(Screen.AddonDiscover.route) },
                colors = ButtonDefaults.colors(
                    containerColor = Color(0xFFFFCC00),
                    contentColor = Color.Black,
                ),
            ) {
                Text("Go to Addons")
            }
        }
    }
}
```

**Build verification:** `./gradlew :app:compileDebugKotlin` — 0 errors.

---

## Fix C: Profile Creation — Focus Trap

**File:** `ui/screens/profiles/ProfileCreateScreen.kt`
**Symptom:** Opening Profile Creation focuses the "Save" button at the bottom. User must D-pad up through PIN fields to reach the Name field. On some devices, focus gets stuck in the PIN fields.

**Root cause:** `FocusRequester` was attached to the Save button:
```kotlin
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
    focusRequester.requestFocus()  // focuses bottom button
}
// ...
Button(
    modifier = Modifier.focusRequester(focusRequester),
    onClick = onSave,
) { Text("Save") }
```

**Fix:**
1. Move `focusRequester` to the Name `OutlinedTextField`
2. Add 200ms delay for scrollable container layout
3. Wrap in `try/catch`

```kotlin
val nameFocusRequester = remember { FocusRequester() }

LaunchedEffect(Unit) {
    delay(200)
    try {
        nameFocusRequester.requestFocus()
    } catch (_: IllegalStateException) {
        // Field may not be mounted yet — best-effort
    }
}

OutlinedTextField(
    value = name,
    onValueChange = onNameChange,
    label = { Text("Profile Name") },
    modifier = Modifier
        .fillMaxWidth()
        .focusRequester(nameFocusRequester),
    colors = TextFieldDefaults.colors(
        focusedContainerColor = NexColors.SurfaceHigh,
        unfocusedContainerColor = NexColors.Surface,
    ),
)
```

**Build verification:** `./gradlew :app:compileDebugKotlin` — 0 errors.

---

## Fix D: Addon Discover — Empty onClick

**File:** `ui/screens/addons/AddonDiscoverScreen.kt`
**Symptom:** Pressing Select on a discoverable addon card does nothing. Card appears focused but is unresponsive.

**Root cause:** The outer `Surface` had an empty `onClick` lambda:
```kotlin
Surface(
    onClick = { },  // ← empty lambda swallows D-pad Select
    colors = ClickableSurfaceDefaults.colors(...),
) {
    Column {
        Text(addon.name)
        Button(onClick = onInstall) { Text("+ Add") }  // nested, hard to reach
    }
}
```

On TV, D-pad lands on the outer Surface first. The empty `onClick` consumes the Select press with no action. The inner "+ Add" button requires an additional D-pad right-press to reach.

**Fix:**
```kotlin
Surface(
    onClick = onInstall,  // ← entire card is actionable
    colors = ClickableSurfaceDefaults.colors(...),
) {
    Column {
        Text(addon.name)
        // Optional: keep inner button for visual indication
        Button(onClick = onInstall) { Text("+ Add") }
    }
}
```

**Build verification:** `./gradlew :app:compileDebugKotlin` — 0 errors.

---

## Fix E: Trakt Connect — No-Op Handler

**Files:** `ui/screens/settings/SettingsTraktScreen.kt`, `ui/screens/settings/SettingsViewModel.kt`
**Symptom:** Settings → Trakt → "Connect Trakt" button does nothing.

**Root cause:** The screen's `onClick` was a literal comment:
```kotlin
Button(
    onClick = { /* Opens OAuth flow */ },
) { Text("Connect Trakt") }
```

The ViewModel had no auth state machine or polling logic.

**Fix — full device-code flow implementation:**

### ViewModel changes:
```kotlin
sealed class TraktAuthState {
    data object Idle : TraktAuthState()
    data class Pending(
        val verificationUrl: String,
        val userCode: String,
    ) : TraktAuthState()
    data object Connected : TraktAuthState()
    data class Error(val message: String) : TraktAuthState()
}

class SettingsViewModel @Inject constructor(
    private val traktAuthManager: TraktAuthManager,
) : ViewModel() {
    private val _traktAuthState = MutableStateFlow<TraktAuthState>(TraktAuthState.Idle)
    val traktAuthState: StateFlow<TraktAuthState> = _traktAuthState.asStateFlow()

    fun startTraktAuth() {
        viewModelScope.launch {
            try {
                val deviceCode = traktAuthManager.startDeviceCode()
                _traktAuthState.value = TraktAuthState.Pending(
                    verificationUrl = deviceCode.verificationUrl,
                    userCode = deviceCode.userCode,
                )
                pollTraktToken(deviceCode.deviceCode, deviceCode.interval)
            } catch (e: Exception) {
                _traktAuthState.value = TraktAuthState.Error(e.message ?: "Failed to start auth")
            }
        }
    }

    private suspend fun pollTraktToken(deviceCode: String, intervalSeconds: Int) {
        repeat(60) {  // 5 minutes max
            delay(intervalSeconds * 1000L)
            val token = traktAuthManager.checkDeviceCode(deviceCode)
            if (token != null) {
                _traktAuthState.value = TraktAuthState.Connected
                return
            }
        }
        _traktAuthState.value = TraktAuthState.Error("Authorization timed out")
    }

    fun disconnectTrakt() {
        viewModelScope.launch {
            traktAuthManager.clearToken()
            _traktAuthState.value = TraktAuthState.Idle
        }
    }
}
```

### Screen — inline state-driven UI:
```kotlin
@Composable
fun SettingsTraktScreen(viewModel: SettingsViewModel = hiltViewModel()) {
    val authState by viewModel.traktAuthState.collectAsState()

    when (val state = authState) {
        is TraktAuthState.Idle -> {
            Button(onClick = { viewModel.startTraktAuth() }) {
                Text("Connect Trakt")
            }
        }
        is TraktAuthState.Pending -> {
            Column {
                Text("Go to: ${state.verificationUrl}")
                Text("Code: ${state.userCode}", fontWeight = FontWeight.Bold)
                Text("Waiting for authorization...")
            }
        }
        is TraktAuthState.Connected -> {
            Text("✓ Connected to Trakt")
            Button(onClick = { viewModel.disconnectTrakt() }) {
                Text("Disconnect")
            }
        }
        is TraktAuthState.Error -> {
            Text("⚠ ${state.message}")
            Button(onClick = { viewModel.startTraktAuth() }) {
                Text("Retry")
            }
        }
    }
}
```

**Build verification:** `./gradlew :app:compileDebugKotlin` — 0 errors.

---

## Fix F: NavRail — Duplicate Keys

**File:** `ui/components/NavRail.kt`
**Symptom:** App crashes on startup with `IllegalArgumentException: Key "details/{type}/{id}" was already used`.

**Root cause:** Movies, Shows, and Anime routes all navigate to `Screen.Details`, but the `LazyColumn` used `item.route` as the key:
```kotlin
items(navItems, key = { it.route }) { ... }
```

**Fix:**
```kotlin
items(navItems, key = { it.label }) { ... }  // label is unique per nav item
```

**Build verification:** `./gradlew :app:compileDebugKotlin` — 0 errors.

---

## Summary Table

| Fix | File(s) | Root Cause | Key Pattern |
|-----|---------|-----------|-------------|
| Debrid focus | `SettingsComponents.kt` | Single `LazyColumn` item wrapper | Use `Column + verticalScroll` for fixed panels |
| Home empty | `HomeViewModel.kt`, `HomeScreen.kt` | `getOrElse { emptyList() }` swallowed errors | Explicit `catalogsUnavailable` state + guidance banner |
| Profile focus | `ProfileCreateScreen.kt` | `FocusRequester` on Save button | Move to first input + 200ms delay + try/catch |
| Addon click | `AddonDiscoverScreen.kt` | `onClick = { }` on Surface | Wire real action or remove interactivity |
| Trakt auth | `SettingsTraktScreen.kt`, `SettingsViewModel.kt` | No-op comment in onClick | Device-code state machine with auto-poll |
| NavRail keys | `NavRail.kt` | Duplicate `item.route` keys | Use `item.label` or guaranteed-unique field |

---

## Cross-Reference

- Gotcha #39 (LazyColumn focus) — `nexstream-android-tv-build` SKILL.md
- Gotcha #40 (FocusRequester delay) — `nexstream-android-tv-build` SKILL.md
- Gotcha #41 (empty onClick) — `nexstream-android-tv-build` SKILL.md
- Gotcha #42 (device-code OAuth) — `nexstream-android-tv-build` SKILL.md
- Gotcha #43 (OutlinedTextField import) — `nexstream-android-tv-build` SKILL.md
- Gotcha #44 (empty-state flag) — `nexstream-android-tv-build` SKILL.md
- Trap #38 (duplicate lazy keys) — `android-project-build` skill, also in `nexstream-android-tv-build`
