# Settings Panel Wiring + Advanced Gate Pattern

## Adding a New Panel to the Settings Screen

The SettingsScreen uses a left-rail/right-pane split layout. Adding a new panel requires changes in 3 files:

### 1. `SettingsScreen.kt` — The `SettingsPanel` enum

```kotlin
private enum class SettingsPanel(
    val label: String,
    val subtitle: String,
    val icon: ImageVector,
    val group: String,
    val advancedOnly: Boolean = false,  // gate behind advanced mode
) {
    AIOSTREAMS("AIOStreams Config", "Advanced scrapers and filtering", 
               Icons.Default.Tune, "Advanced", advancedOnly = true),
}
```

Panels with `advancedOnly = true` are hidden unless Advanced Mode is ON.

### 2. SettingsScreen — Panel filtering at render time

```kotlin
val panelsRaw = remember { SettingsPanel.entries.toList() }
val panels = if (settingsState.showAdvancedMode) panelsRaw 
             else panelsRaw.filter { !it.advancedOnly }
```

### 3. SettingsScreen — `when(selected)` block

```kotlin
SettingsPanel.AIOSTREAMS -> AIOStreamsEmbeddedScreen(
    contentFocusRequester = contentFocusRequesters[SettingsPanel.AIOSTREAMS],
)
```

## SettingsToggleRow API Duality

Two `SettingsToggleRow` composables exist with different APIs:

- **`com.unspooled.app.ui.screens.settings.SettingsDesignSystem`** (internal, TV-optimized): `checked: Boolean, onToggle: () -> Unit`
- **`com.unspooled.app.ui.components.SettingsDesignSystem`** (public, simpler): `isChecked: Boolean, onCheckedChange: (Boolean) -> Unit`

Files in `com.unspooled.app.ui.screens.settings` package — use the internal one. Others — use the public one.

## AIOStreams Config Persistence

`AIOStreamsConfigViewModel` loads from assets but saves user modifications to `context.filesDir`:

```kotlin
private val userConfigFile = File(context.filesDir, "aiostreams-user-config.json")

private fun loadConfig() {
    val preset = loadPersistedConfig() ?: configBuilder.loadPreset()
    workingPreset = preset
}
private fun persistAndRefresh() {
    userConfigFile?.writeText(workingPreset.toString(2))
    refreshUi()  // reads from workingPreset in-memory, not from disk
}
```

Critical: `refreshUi()` reads from `workingPreset` (in-memory), NOT from file — prevents the re-read-from-assets bug.

## RemoteInputService (AccessibilityService)

Android service that polls an endpoint and injects received text into the focused input field.

### Manifest Declaration
```xml
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<service android:name=".service.RemoteInputService" android:exported="true"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data android:name="android.accessibilityservice"
        android:resource="@xml/remote_input_service_config" />
</service>
```

### Config XML (`res/xml/remote_input_service_config.xml`)
```xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:canRetrieveWindowContent="true"
    android:description="@string/remote_input_service_description"
    android:notificationTimeout="100" />
```

### Core Loop
```kotlin
class RemoteInputService : AccessibilityService() {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val client = OkHttpClient.Builder()
        .connectTimeout(3, TimeUnit.SECONDS)
        .build()

    override fun onServiceConnected() {
        serviceInfo = AccessibilityServiceInfo().apply {
            eventTypes = AccessibilityEvent.TYPES_ALL_MASK
            feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
        }
        startForeground(NOTIFICATION_ID, buildNotification())
        scope.launch { while (isActive) { pollLoop() } }
    }

    private suspend fun pollLoop() {
        while (isActive) {
            try {
                val text = pollForText() // GET /api/remote/pending
                if (text.isNotEmpty()) injectText(text)
            } catch (_: Exception) { }
            delay(1500L)
        }
    }

    private fun injectText(text: String) {
        val focused = rootInActiveWindow?.findFocus(
            AccessibilityNodeInfo.FOCUS_INPUT
        ) ?: return
        val args = Bundle().apply {
            putCharSequence(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
        }
        focused.performAction(ACTION_SET_TEXT, args)
        focused.recycle()
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
    override fun onInterrupt() {}
}
```

### Design Decisions
- 1.5s polling — low CPU, responsive
- ACTION_SET_TEXT replaces all text (correct for search inputs)
- Clipboard fallback for WebViews if ACTION_SET_TEXT fails
- Foreground notification prevents Android from killing it
- User enables in TV Settings → Accessibility → app name
