# NuvioTV → NexStream UI Integration Pattern

**Session:** 2026-05-24 — surgical copy of NuvioTV UI into NexStream
**Result:** 7 commits, 89 files, +7.4K loc, build ✅, 63/63 tests ✅

## The Core Problem

NuvioTV (530 .kt files) and NexStream (209 .kt files) both target Android TV
with Jetpack Compose, but their **domain models are incompatible**:

| Concept | NuvioTV | NexStream |
|---------|---------|-----------|
| Content type | `ContentType` enum | `String` ("movie"/"series") |
| MetaPreview | Rich (Float imdbRating, computed backdropUrl) | Simple (String imdbRating, no computed props) |
| PosterShape | `PosterShape` enum (required field) | `String?` (nullable) |
| Backend | Supabase + Stremio addon protocol | Direct TMDB/Trakt/JustWatch API |
| Auth | Supabase Auth | Local PIN + Trakt OAuth |

**Wholesale file replacement failed** — KSP symbol processing broke on
`ContentType` and `PosterShape` type mismatches in NexStream's data layer.

## The Surgical Copy Pattern

### Tier 1: Direct Copy (no deps on domain models)
Files that only reference the theme (`NuvioColors`), standard Compose, and
Android SDK can be copied directly with `sed` package rename.

```bash
sed 's/com\.nuvio\.tv/com.nexstream.app/g' $src > $dst
```

**Successfully copied (v1.21):**
- `Color.kt`, `Theme.kt`, `ThemeColors.kt`, `Type.kt` — theme system
- `NuvioDialog.kt` — modal dialog with key suppression
- `EmptyScreenState.kt` — icon + title + subtitle empty state
- `Skeletons.kt`, `PlaceholderShimmer.kt` — loading skeletons
- `LoadingIndicator.kt` — animated dots loader
- `PosterCardDefaults.kt`, `ProfileAvatarCircle.kt`, `MonochromePosterPlaceholder.kt`
- `NuvioScrollDefaults.kt`, `SourceStatusFilterChip.kt`
- 11 UI utility files (DpadFastScroll, DpadThrottle, StableHolders, etc.)
- Fonts (Inter, DM Sans, Open Sans .ttf), 2,480 string resources, 22 SVG icons

### Tier 2: Copy + Remove Adapted Dependencies
Files that reference NuvioTV-specific classes but those references are in
optional/removable sections.

**Example — InAppYouTubeExtractor (865 lines):**
- Only used `Gson` for JSON — added `com.google.code.gson:gson:2.11.0` to build.gradle.kts
- `BuildConfig` import resolved automatically after package rename
- No other NuvioTV-specific deps
- Compiles cleanly

### Tier 3: Adapt (rewrite against NexStream models)
Take the UI pattern from NuvioTV, reimplement against NexStream's models.

**NexHeroCarousel.kt** — adapted from NuvioTV's HeroCarousel:
- Uses NexStream's `MetaPreview` (String imdbRating, nullable genres)
- Backdrop = `background ?: poster` (no `backdropUrl` computed property)
- Wired into `HomeScreen.kt` replacing `AutoHeroBanner`
- Adds crossfade transitions, auto-advance, IMDB badge, genre chips, gradient overlay

**SettingsScreen.kt** — rebuilt from scratch using NuvioTV theme:
- Split-pane layout (left header, right list)
- Icon-led `SettingsItem` with focus rings
- Navigates to existing NexStream screens (Addons, History, Watchlist, etc.)
- Wired into NavHost at `/settings`

### Tier 4: Skip (too coupled to NuvioTV data layer)
Any file with these markers is a skip:
- `@HiltViewModel` with NuvioTV-specific `@Inject` constructor params
- References to `Supabase`, `Stremio`, `com.nuvio.tv.core.*`, `com.nuvio.tv.data.*`
- `hiltViewModel<SomeNuvioViewModel>()` calls
- Domain model imports that conflict (ContentType, PosterShape, MetaPreview)

**Nuked successfully:**
- `SettingsViewModel.kt` — 990-line god object (replaced by new SettingsScreen)
- 13 settings sub-screens with Hilt VM deps
- `TrailerService.kt` — needs TMDB/Trakt data layer adaptation

## Build Discipline

After each integration batch:
```bash
cd /home/rurouni/nexstream
./gradlew :app:compileDebugKotlin  # must pass
./gradlew :app:test                 # 63/63 must pass
git add -A && git commit -m "feat: ..."
```

This catches domain model conflicts at the FIRST batch rather than accumulating
100+ errors across 239 files.

## Pitfalls

1. **`sed` package rename is not enough** — it handles `com.nuvio.tv` →
   `com.nexstream.app` but does NOT fix `R.string.xxx` / `R.drawable.xxx`
   references. NuvioTV resources may not exist in NexStream. Always verify
   resource references after copy.

2. **KSP processes ALL @HiltViewModel files** — even if the file isn't imported
   anywhere. A single ViewModel with a non-existent constructor parameter will
   fail the entire build. Remove ALL ViewModel files from NuvioTV immediately
   after copy.

3. **Domain model conflicts cascade** — changing `ContentType` from enum to
   String breaks every file that constructs `MetaPreview`. Never copy NuvioTV's
   domain models; always adapt UI to NexStream's existing models.

4. **Resources are large but necessary** — NuvioTV's strings.xml has 2,480
   entries. Copying it wholesale avoids hundreds of individual `R.string.*`
   resolution errors.

5. **The SettingsViewModel was a 990-line god object** — it was nuked, not
   adapted. The replacement SettingsScreen is ~200 lines and uses NexStream's
   existing navigation + NuvioTV's theme.

## Tier 5: ViewModel Bridge Pattern (2026-05-27 Session)

When a NuvioTV screen references `@HiltViewModel` with NuvioTV-specific deps
(Retrofit APIs, config servers, sync services), create a **bridge ViewModel**:

1. Write a clean ViewModel in UNSPOOLED's package that exposes the same
   `uiState: StateFlow<SomeUiState>` and public method signatures the screen
   composable expects.
2. Use UNSPOOLED's existing DataStores (`DebridSettingsDataStore`,
   `CollectionsDataStore`, etc.) as the data source.
3. Stub out unavailable features (device auth, QR formatter, config servers)
   rather than trying to copy NuvioTV's entire backend.
4. Keep the NuvioTV `UiState` data class verbatim as a nested class in the
   ViewModel file — it defines the contract with the screen composable.

**Example — DebridSettingsViewModel bridge:**
- Screen calls `viewModel.validateAndSaveProviderApiKey(id, value, onSuccess)`
- Bridge VM accepts any non-empty key (skip Retrofit-based validation)
- Screen calls `viewModel.startFormatterQrMode()` — stubbed with error message
- `resolverProviders`, `activeResolverProvider`, `hasResolverProvider` computed
  from `DebridServiceProviders` object + stored API keys

## Provider Registry Pattern

NuvioTV has a `DebridProviders` object singleton with `visible()`, `byId()`,
`configuredServices()`, etc. Copying it verbatim causes **name clashes**:
UNSPOOLED already has an `enum class DebridProvider` in `Models.kt`.

**Rename to avoid clashes:** `DebridProvider` → `DebridServiceProvider`,
`DebridProviders` → `DebridServiceProviders`, `DebridProviderCapability` →
`DebridServiceCapability`, `DebridProviderAuthMethod` → `DebridServiceAuthMethod`.

The renamed object lives at `com.nexstream.app.core.debrid.DebridProvider.kt`.
It only depends on `DebridSettings.apiKeyFor(providerId)` — add support for
`"realdebrid"` (without underscore) alongside the existing `"real_debrid"` mapping.

## Mechanical Screen Porting (sed/regex approach)

For screens without domain model conflicts (like DebridSettingsScreen, 1472 lines):

```
1. Copy source, apply sed for package rename (com.nuvio.tv → com.nexstream.app)
2. Apply type renames: DebridProviders→DebridServiceProviders, etc.
3. Replace NuvioColors→NexColors, NuvioDialog→SettingsDialogWrapper
4. Replace R.string.* references with placeholder strings
5. Stub missing composables (QrCodeOverlay, LayoutPreview) as empty Box
6. Build and fix remaining errors iteratively
```

**Pitfall:** `stringResource(R.string.*)` references survive sed because they
reference `com.nexstream.app.R`, not `com.nuvio.tv.R`. Use regex to replace
all of them with inline strings, then restore the ones UNSPOOLED has.

## Three Porting Strategies (choose per-screen)

| Strategy | When | Cost | Result |
|----------|------|------|--------|
| **Clean port** | Screen is small or UI pattern differs | 2-3h | Rewritten against UNSPOOLED infra |
| **Partial bridge** | Screen is large, ViewModel is the blocker | 1-2h | Screen code kept, ViewModel bridged |
| **Full migration** | User demands NuvioTV backend | 10h+ | 40+ infra files copied, DI graph rebuilt |

**Partial bridge is the sweet spot** — it preserves the screen's UI while
replacing only the data layer. This is what worked for DebridSettingsScreen.

## Post-Copy Fixes (2026-05-24 Session)

These issues emerged after the initial surgical copy. Each has a known fix:

### Buggy Sidebar (can't click out, shadow too heavy)
**Root cause**: NexStream's `NetflixSideNavigation` (403 lines) used `graphicsLayer`,
`zIndex`, `Brush` shadows, and complex focus group logic.
**Fix**: Replaced with `NexSidebar` (178 lines) using NuvioTV's clean pattern:
`animateIntOffsetAsState` slide + `onFocusChanged` auto-collapse + `Card(onClick=)`
for nav items. No shadows, no z-layers. Wired into `NetflixHomeScreen.kt` as a
`Row` with sidebar on left, content on right.

### Settings Items Not Clickable
**Root cause**: `focusable()` + `onFocusChanged` provides visual focus but
doesn't handle D-pad Center/Enter key events.
**Fix**: Added `onPreviewKeyEvent` handler checking for `Key.DirectionCenter`
and `Key.Enter` on `KeyEventType.KeyUp`. Alternatively, wrap in `Card(onClick=)`.

### Trailer Playback Crashes
**Root cause**: Trailer URL is a YouTube webpage URL, not a media stream URL.
ExoPlayer/Media3 can't play it directly.
**Fix**: Changed `onPlayTrailer` in `DetailsScreen.kt` to open YouTube externally
via `Intent(ACTION_VIEW, Uri.parse(url))` with `FLAG_ACTIVITY_NEW_TASK`.
Proper in-app playback requires wiring `InAppYouTubeExtractor` through the player.

### MyList Shows Empty
**Root cause**: Filter for specific row IDs (`top_rated`, `trending`) that
don't exist in `MoviesViewModel`'s data.
**Fix**: Removed the filter — `uiState.rows.map { ... }` shows all available rows.

### Theme Swap: NexColors → NuvioColors
**Technique**: Use `sed` for bulk `NexColors.` → `NuvioColors.` replacement,
then map missing tokens:
```
NexColors.Accent       → NuvioColors.Secondary
NexColors.AccentSoft   → NuvioColors.Secondary.copy(alpha = 0.3f)
NexColors.BackgroundRaised → NuvioColors.BackgroundElevated
NexColors.SurfaceHigh  → NuvioColors.SurfaceVariant
NexColors.SurfaceTop   → NuvioColors.Surface
```
Rebuild immediately after swap to catch unresolved references.
