# NuvioTV Screen Clean Rewrite Pattern (May 2026)

## When to use this (Option A)

The bridge/port approach (references/nuvio-screen-port-pattern.md) copies NuvioTV screens mechanically.
This is **Option A — Clean Adaptation**: write the screen from scratch using UNSPOOLED's
existing design system, data layer, and ViewModels. Same functionality, UNSPOOLED-native code.

**Prefer this when:**
- The screen has significant architectural differences from its NuvioTV counterpart
- You want zero NuvioTV coupling (no adapter ViewModels, no shims, no DI bridges)
- The UNSPOOLED data layer already supports the feature (e.g. DebridSettingsDataStore exists)

## Process

### 1. Study NuvioTV's feature set

Read the full NuvioTV screen + ViewModel to understand every dialog, every setting,
every enum value. The goal is feature parity, not code parity.

```
NuvioTV-0.6.21-beta/app/src/main/java/com/nuvio/tv/ui/screens/settings/DebridSettingsScreen.kt   (1472 lines)
NuvioTV-0.6.21-beta/app/src/main/java/com/nuvio/tv/ui/screens/settings/DebridSettingsViewModel.kt
```

### 2. Audit UNSPOOLED's existing infrastructure

- What data store exists? (`DebridSettingsDataStore` — flat JSON in Preferences DataStore)
- What design system components exist? (`SettingsDesignSystem.kt` — SettingsToggleRow, SettingsActionRow, SettingsSingleChoiceDialog, SettingsMultiChoiceDialog, etc.)
- What domain models exist? (`DebridSettings.kt`, `DebridStreamPreferences`, enums)
- What navigation patterns? (`Screen.kt`, `NexStreamNavHost.kt`)

### 3. Define types in the ViewModel (not the Screen)

All shared types go in the ViewModel file — keep the Screen file composable-only:

```kotlin
// DebridSettingsViewModel.kt — shared types at bottom
data class DebridSettingsUiState(...)
object DebridProviders { ... }
enum class DebridProviderAuthMethod { ... }
enum class DebridSortProfile { ... }
enum class DebridStreamPicker { ... }
sealed class DebridSettingsEvent { ... }
```

**Pitfall:** Defining the same types in both Screen.kt and ViewModel.kt causes
"Unresolved reference" and "cannot be invoked as a function" errors. Types must
live in exactly one file.

### 4. Write the Screen using UNSPOOLED's design system

Use the existing internal components directly — they're in the same package:

- `SettingsToggleRow` — toggle switches
- `SettingsActionRow` — navigable rows with value display
- `SettingsSingleChoiceDialog` — single-pick dialogs
- `SettingsMultiChoiceDialog` — multi-pick dialogs
- `SettingsDialogWrapper` — dialog container (must be `internal`, see below)
- `SettingsVerticalScrollIndicators` — scroll indicators (BoxScope extension)
- `SettingsDialogActionRow`, `SettingsDialogActionButton` — dialog button bars

Text fields use `BasicTextField` directly with `Card` for styling (follow the
NuvioTV pattern but use `NuvioTheme.colors.*`).

### 5. Wire into navigation

```kotlin
// Screen.kt — add route
data object DebridSettings : Screen("settings/debrid-settings")

// NexStreamNavHost.kt — add composable entry
composable(route = Screen.DebridSettings.route, ...) {
    DebridSettingsScreen(onBack = { navController.popBackStack() })
}

// SettingsScreen.kt — add sidebar rail item
RailItem("Debrid Settings", Icons.Default.Settings, Screen.DebridSettings.route)
```

### 6. Fix visibility blockers

`SettingsDialogWrapper` is `private` in SettingsDesignSystem.kt by default.
Change to `internal` so sub-screens can use it:

```bash
sed -i 's/private fun SettingsDialogWrapper(/internal fun SettingsDialogWrapper(/' \
  app/src/main/java/com/nexstream/app/ui/screens/settings/SettingsDesignSystem.kt
```

## Pitfalls

1. **Duplicate types across Screen and ViewModel**: Define shared types (UiState,
   DebridProviders, enums) in the ViewModel file only. Same-package Kotlin
   resolves them automatically. Having a `private` duplicate in the Screen
   file creates a name collision.

2. **`visible` is a property, not a function**: `DebridProviders.visible` is
   `val visible = all` — call it as `DebridProviders.visible` not `.visible()`.

3. **Missing DataStore methods**: The ViewModel's `saveProviderCredential()`
   needs a generic `setProviderApiKey(providerId, value)` on the DataStore.
   Add it:
   ```kotlin
   suspend fun setProviderApiKey(providerId: String, value: String) {
       mutate { settings ->
           when (providerId.lowercase()) {
               "torbox" -> settings.copy(torboxApiKey = value)
               "premiumize" -> settings.copy(premiumizeApiKey = value)
               "real_debrid" -> settings.copy(realDebridApiKey = value)
               else -> settings
           }
       }
   }
   ```

4. **Project path confusion**: The canonical project is at `/home/rurouni/nexstream/`.
   Do NOT write to `/home/rurouni/.hermes/hermes-agent/nexstream/` (that's a
   stale/partial directory). Always verify with `ls` before writing.

## Verified working example

DebridSettingsScreen (1,218 lines) — clean rewrite, May 2026:
- API key entry for 3 providers with masked display
- Cloud Library toggle
- Instant playback preparation (enable + count)
- 12 filter categories × 3 modes (preferred/required/excluded):
  resolutions, qualities, visual tags, audio tags, audio channels,
  encodes, languages, release groups
- Sort profile presets (Default, Largest, Smallest, Audio, Language)
- Size range picker, max results picker, per-resolution/quality limit pickers
- Release group text list dialog
- QR formatter stub (requires NanoHTTPD + QR libs)
- Device auth stub (requires TorboxApi, PremiumizeApi)
- 131 tests pass, APK builds green
