package com.example.aiapp import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp /** * One `when` rather than a navigation library: four screens, with the list as the root and the back * button the only other way between them. */ private sealed class Screen { data object SessionList : Screen() data class Session(val summary: SessionSummary) : Screen() data object Spawn : Screen() data object Usage : Screen() data object Models : Screen() data object Setups : Screen() data object Settings : Screen() } /** * [settingsVersion] bumps when enrollment lands via an `aiapp://` intent (see MainActivity), * re-reading the stored settings -- a plain `remember` would keep serving the pre-enrollment null. */ @Composable fun AppRoot(settingsVersion: Int) { val context = LocalContext.current var settings by remember(settingsVersion) { mutableStateOf(loadServerSettings(context)) } var screen by remember { mutableStateOf(Screen.SessionList) } // Bumped whenever another screen changes something the list shows, so // returning to it refetches instead of showing a stale list. var reloadToken by remember { mutableIntStateOf(0) } // A standing condition rather than a per-request failure, so it is // stated once here instead of appended to every error that might be // caused by it. Without this the app is simply unreachable and every // screen blames the server or the tunnel for it. if (!localNetworkAllowed(context)) { Text( "This app is not allowed to reach local network addresses, so it cannot " + "connect to the backend at all. Grant \"local network\" in Android's app " + "settings; until then every screen here will look like the server is down.", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(16.dp), ) } val current = settings if (current == null) { // Not enrolled yet: settings is the only usable screen. The QR // path lands in MainActivity and recomposes from the top. SettingsScreen( existing = null, onSaved = { saved -> settings = saved screen = Screen.SessionList }, onBack = null, ) return } // The one way back, whichever screen is showing and whether it was // reached by the system back gesture or a screen's own Back button. // Every leaf screen can have changed something the list shows, so it // always refetches. val goToList = { reloadToken++ screen = Screen.SessionList } if (screen !is Screen.SessionList) { BackHandler(onBack = goToList) } when (val here = screen) { is Screen.SessionList -> SessionListScreen( settings = current, reloadToken = reloadToken, onOpen = { screen = Screen.Session(it) }, onSpawn = { screen = Screen.Spawn }, onUsage = { screen = Screen.Usage }, onModels = { screen = Screen.Models }, onSetups = { screen = Screen.Setups }, onSettings = { screen = Screen.Settings }, ) is Screen.Session -> SessionScreen( settings = current, summary = here.summary, onBack = goToList, ) is Screen.Spawn -> SpawnScreen( settings = current, onSpawned = { spawned -> reloadToken++ screen = Screen.Session(spawned) }, onBack = goToList, ) is Screen.Usage -> UsageScreen(settings = current, onBack = goToList) is Screen.Models -> ModelsScreen(settings = current, onBack = goToList) is Screen.Setups -> SetupsScreen(settings = current, onBack = goToList) is Screen.Settings -> SettingsScreen( existing = current, onSaved = { saved -> settings = saved goToList() }, onBack = goToList, ) } }