# Session 2026-05-20 — SeeAll / AllSources ViewModels + SettingsVM Combine Refactor

## Session Context
- Model: kimi-k2.6 via OpenCode Go
- Starting state: 0 compile errors, all critical/high audit findings fixed
- Remaining: H1 (SeeAll stub), H2 (AllSources stub), M1 (SettingsVM 65-flow combine on Main)

## H1: SeeAllScreen ViewModel + Grid Implementation

Created `SeeAllViewModel.kt` following the `DetailsViewModel` pattern:
- `SavedStateHandle` extracts nav args (`catalogType`, `catalogId`)
- Injects `AddonRepository`, calls `getCatalogRows(type, id)`
- Exposes `StateFlow<SeeAllUiState>` with loading/error/items states

Rewrote `SeeAllScreen.kt` from stub to full implementation:
- Uses `LazyVerticalGrid(columns = GridCells.Fixed(5))` for TV-optimized browsing
- Reuses existing `MediaCard` component for D-pad focusable poster cards
- Integrates `LoadingMessage` (shimmer skeleton) for loading state
- Error state with Retry button (`androidx.tv.material3.Button`)
- Navigation to Details on card selection via `Screen.Details.createRoute()`

## H2: AllSourcesScreen ViewModel + Ranked Source List

Created `AllSourcesViewModel.kt`:
- `SavedStateHandle` extracts `type` and `id` nav args
- Injects `AddonRepository`, `SourceRanker`, `PreferencesManager`
- Fetches streams via `addonRepository.getStreamsForContent(type, id)`
- Builds addon health map from `addonRepository.addons.value`
- Assembles `SourcePreferences` from current settings (`preferDebridCached.first()`, `prefer4kDebrid.first()`)
- Ranks streams via `sourceRanker.rankStreams(streams, healthMap, userPrefs)`
- Exposes `StateFlow<AllSourcesUiState>` with loading/error/rankedStreams

Rewrote `AllSourcesScreen.kt` from stub to full implementation:
- Uses `LazyColumn` with numbered rank rows
- Each row shows: rank number, stream name, ⚡ cached badge, resolution badge (4K/1080p/720p), addon name
- Navigation to Player on selection via `Screen.Player.createRoute(type, id, streamIndex)`
- Empty state with helpful message

## M1: SettingsViewModel Combine Refactor

**Problem:** `init` block wrapped `combine(...65 flows...).stateIn(...)` inside `viewModelScope.launch`. This:
1. Blocked Main thread during initial collection (all 65 DataStore reads + mapping)
2. Used `stateIn()` incorrectly inside `launch` — `stateIn` returns a StateFlow, which was never collected
3. Never re-emitted when `_debridAccountInfos` changed because it wasn't part of the combine

**Fix — three changes:**

### 1. Move combine execution to IO dispatcher
```kotlin
// BEFORE (runs on Main):
viewModelScope.launch {
    combine(...65 flows...) { args -> ... }
        .stateIn(viewModelScope, SharingStarted.Eagerly, SettingsUiState())
}

// AFTER (runs on IO, updates _uiState on Main via onEach):
combine(...65 flows..., _debridAccountInfos) { args ->
    // ...build SettingsUiState...
}
    .flowOn(Dispatchers.IO)          // ← all DataStore reads + mapping off Main
    .onEach { _uiState.value = it }  // ← emit result back to Main thread
    .launchIn(viewModelScope)        // ← collect in viewModelScope
```

### 2. Include secondary StateFlow as 66th combine input
```kotlin
// BEFORE: _debridAccountInfos loaded asynchronously but was NOT in the combine
// Result: account info loaded, UI never updated until some OTHER preference changed

// AFTER: _debridAccountInfos added as the 66th flow
combine(
    preferencesManager.enableAutoPlay,
    // ...64 other flows...
    preferencesManager.traktTokenExpiresAt,
    _debridAccountInfos,              // ← 66th flow
) { args ->
    // args[65] is the debrid account info map
    val debridAccountInfos = args[65] as? Map<DebridProvider, DebridAccountInfo?> ?: emptyMap()
    // ...
    debridProviders = buildDebridProviderState(debridAccountInfos),
}
```

### 3. Pass account info map into builder instead of reading from internal StateFlow
```kotlin
// BEFORE (stale read — reads from _debridAccountInfos.value at combine-emission time,
// but the value might not have been updated yet):
private fun buildDebridProviderState(): List<DebridProviderState> {
    // ... reads _debridAccountInfos.value[provider] ...
}

// AFTER (receives the current map from the combine lambda):
private fun buildDebridProviderState(
    accountInfos: Map<DebridProvider, DebridAccountInfo?> = emptyMap()
): List<DebridProviderState> {
    // ... reads accountInfos[provider] ...
}
```

## Null-Safety Compile Fix

`Stream.addonName` is nullable (`String?`). The AllSourcesScreen initially used:
```kotlin
if (stream.addonName.isNotBlank()) {  // ERROR: only safe calls allowed on nullable
    Text(text = stream.addonName)     // ERROR: String? expected String
}
```

Fixed with:
```kotlin
if (!stream.addonName.isNullOrBlank()) {
    Text(text = stream.addonName ?: "")
}
```

## Build Verification
- `compileDebugKotlin`: 0 errors, 1 warning (unchecked cast in combine lambda — harmless)
- No runtime crashes introduced
- All new screens follow existing navigation patterns (SavedStateHandle → ViewModel → Screen)
