# Unspooled — Project Context

## Identity
Building a Stremio-compatible debrid streaming Android TV app. TV-optimized UI with Compose for TV. 35+ screens, 45 scrapers, 82 extractors, 5 debrid providers.

## Build
```
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
cd ~/Unspooled && ./gradlew :app:assembleDebug
# APK → app/build/outputs/apk/debug/app-debug.apk (~65 MB)
# Build time ~18s with config cache
```

## Tech Stack
Kotlin 2.1.0 · Jetpack Compose TV (tv-material3 1.1.0) · Media3 1.8.0 · Hilt 2.53.1 · Room 2.7.1 · Coil 3 · Gradle 8.13

**Services:** nexstream-scraper v3 (Docker, port 3091) · addon-config-worker (Cloudflare Workers)

## Architecture
- **Package**: `com.unspooled.app`
- **Pattern**: Domain/data/di/ui layers with Hilt DI
- **Navigation**: UnspooledNavHost, Screen sealed class routes
- **State**: ViewModel + StateFlow per screen, Room for persistence
- **Auth**: 5-profile system, Supabase backend, debrid OAuth (AES-256-GCM)
- **Scrapers**: Stremio addon protocol → nexstream-scraper → debrid resolve

## Key Design Decisions
- **No springs on cards** — use `tween(200, LinearOutSlowIn)` for focus animations. Springs cause full re-layout on TV.
- **Zero `moveFocus()`** — use FocusGraph, FocusLinks, TvFocusZone state machine
- **`graphicsLayer`** for card focus scale (Compose 1.10.3 has no `Modifier.scale()`)
- **`requestFocusAfterFrames()`** instead of raw `requestFocus()`
- **Motion tokens**: Card=200ms, Default=220ms, CardExpand=280ms, FocusScale=1.05f
- **Sidebar icons**: `Color.White.copy(alpha = 0.55f)` minimum. Red accent bar (3dp) + white focus bg.
- **NuvioTheme**: Dark-only, 9 semantic colors, UnifiedTokens for all dimensions
- **Lifecycle-based focus restore**: use `DisposableEffect` + `LifecycleEventObserver(ON_RESUME)` to re-trigger `LaunchedEffect` for focus restoration after navigation pop (same pattern as `SeeAllScreen`)

## Pitfalls
- `Modifier.onClick()` requires `@OptIn(ExperimentalFoundationApi::class)` — use `clickable`
- `Modifier.shadow()` needs explicit `import androidx.compose.ui.draw.shadow`
- TV Material3 has no `Modifier.scale()` — use `graphicsLayer`
- Avoid `import androidx.compose.material3.MaterialTheme` — use `androidx.tv.material3.MaterialTheme`
- RealDebrid `instantAvailability` is DEPRECATED — use addMagnet + poll
- Build: `NEXSTREAM_SCRAPER_URL` must be set in `local.properties`
- **Never use `selectableGroup()` on TV** — use `focusGroup()`. `selectableGroup()` adds `Role.TabList` semantics that block D-pad vertical traversal
- **Never use `focusRestorer` on non-focusable containers** — `FocusRequester.Default` fallback creates dead-end focus chains; inner `focusGroup()` handles restoration
- **Don't mirror `selectedIndex` as `focusedIndex`** — resetting focus state on recomposition pins focus to selected item, making it appear "stuck"
- `event.nativeKeyEvent` is a property on Compose `KeyEvent` — NO separate import needed
- `Key.DirectionCenter` is the correct enum for D-pad center/enter button (not `Key.DpadCenter`)
- `canLoadMore = hasMore` — do NOT gate on `supportsSkip`; `mergeCatalogPage` handles duplicate detection
- **Focus restore on Back** — `LaunchedEffect(restoreRowId, restoreIndex, rows.size)` alone won't re-trigger after navigation pop; add `shouldRestoreFocus` key guarded by a `LifecycleEventObserver`

## Current State
**UI Audit: 90/100 + simplified Addons + genre chips + focus/trailer cleanup (Jul 03).**

### Simplified addons + focus cleanup (Jul 03)
- **No addon marketplace/presets** — removed curated marketplace, discoverable addons, addon preset registry UI, and AIOStreams config/provider screens.
- **Manual addon URL remains** — Addons screen keeps `+ Add Addon URL`, phone URL entry, installed/system addon management, refresh, enable/disable, and user-addon remove.
- **AIOStreams removed** — no longer seeded or auto-enabled; existing `aiostreams` system rows are purged via `REMOVED_SYSTEM_IDS`. `nexstream_scraper` remains the built-in source engine.
- **Addon dedupe** — `AddonRepository.mergeAddonLists()` dedupes merged system/user addons by id and normalized manifest URL.
- **Trailer autoplay removed** — poster focus no longer loads/plays hero trailer previews; trailers should only play from explicit user actions.
- **Focus fixes** — Show More tile no longer cancels RIGHT, Debrid Services routes detail focus to provider action chips, and Clear Cache restores focus inside Advanced settings.
- **Genre chips** — Home/Movies/TV Shows chips now use `All`, `Comedy`, `Horror`, `Sci-Fi`, `Action`, `Romance`, `Docs` instead of source labels.

### Source filter chips + focus restoration (Jun 26)
- **Movies/TV Shows chips** — `HomeSourceFilterChips` now renders in Movies and TV Shows pages (above catalog grid, below page title), matching the Home page experience
- **Client-side filtering** — `allRows` backing field in both ViewModels; `filterRows()` applies genre/category filter without HTTP re-fetch
- **`TmdbCatalogDefinition.resolveSource()`** — consolidated source detection logic shared across Home, Movies, TV Shows ViewModels
- **`heroUpFocusRequester` conditional** — null when no hero, enabling natural D-pad UP from first catalog row to chips
- **Details Back focus restoration** — `CatalogBrowserScreen` now uses `LifecycleEventObserver(ON_RESUME)` + `shouldRestoreFocus` flag to re-trigger `CatalogGridHost`'s `LaunchedEffect` focus restore when returning from Details/meta screens

### Settings cleanup (Jun 26)
- **Page shell** — Settings now uses a page-level gradient background and fixed large `Settings` title; detail panels no longer repeat the page title.
- **DPAD LEFT** — Content panel explicitly routes LEFT back to the category rail.
- **Fake settings cleanup** — Removed hardcoded empty-handler Playback rows: Default player, Hardware decoding, Audio passthrough, Preferred audio language, Subtitle behavior, Refresh-rate matching, Decoder options.
- **Advanced safety** — Clear Cache now requires confirmation and reports success/failure.
- **Read-only semantics** — Disabled diagnostics rows no longer show chevrons.
- **Visual consistency** — Settings toggles/chips use accent tokens and white focused-card treatment; Trakt consumes the Settings content focus requester.
- **Addon focus safety** — Addons loading skeletons are disabled and action buttons no longer cancel unspecified DPAD directions.

### Previous UI audit (Jun 23)
All 7 audit items complete:
1. **Hero/catalog source parity** — `loadPriorityRows()` branches catalog IDs by source (CINEMETA→`"top"`, TMDB_NATIVE→keeps existing)
2. **See All pagination** — `canLoadMore = hasMore` (removed `&& supportsSkip`); `initialCatalogHasMore` no longer short-circuits for non-skip addons
3. **Sources entry** — Long-press `DPAD_CENTER` on catalog cards navigates to `AllSourcesScreen`
4. **Source filter chips** — `LazyRow`+`focusGroup()` filter chips in ModernHomeContent, D-pad UP from catalog rows
5. **Focus dead-end fix** — `registrar.reset()` on cross-section `onNavigate` prevents premature focus on old content
6. **Sidebar fixed** — `selectableGroup`→`focusGroup`, removed `focusRestorer`/`focusedNavIndex`, stable focus across all 7 items
7. **Hardcoded values removed** — Replaced `32.dp`/`24.dp` with `UnifiedTokens` in CatalogBrowserScreen; cleaned unused `dp`/`sp` imports in CatalogRail

All `dp`/`sp` values in catalog screens use `UnifiedTokens`.

### Remaining
- Settings row-level focus restoration per category after dialogs/subpages
- Decide whether to integrate or remove remaining unreachable standalone `settings/...` routes (TMDB, Navigation Style)
- Quick action rail on expanded cards (Play / My List / Trailer / Info)
- Crossfade on route transitions
- Onboarding flow
- Anime page — proper ViewModel with real anime sources or polished empty state

## Key Code Patterns
- `HomeRow` (line 340): `id`, `title`, `type: RowType`, `items: List<MetaPreview>`, `catalogType`, `catalogId`, `hasMore`, `supportsSkip`, `currentPage`, `skipStep`, `nextSkip`, `consecutiveDuplicatePages`
- Chip filter labels: `"all"`, `"comedy"`, `"horror"`, `"scifi"`, `"action"`, `"romance"`, `"documentary"` — genre/category filtering, persisted through the legacy home source filter preference key
- `registrar.reset()` in `onNavigate` callback (LegacySidebarScaffold) detaches stale focus state on section change
- Focus restore: `LifecycleEventObserver(ON_RESUME)` → `shouldRestoreFocus` flag → `LaunchedEffect` key in `CatalogGridHost`
