Files
iris_x_hermes/app/shared/src/commonMain/kotlin/iris/IrisApp.kt
T

107 lines
4.4 KiB
Kotlin

package iris
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import iris.data.SecureStore
import iris.net.GatewayClient
import iris.platform.setActiveController
import iris.state.IrisController
import iris.ui.ArtifactPreviewOverlay
import iris.ui.screens.ChatScreen
import iris.ui.screens.ConnectScreen
import iris.ui.theme.IrisColors
import iris.ui.theme.IrisTheme
import iris.ui.theme.LocalUserTheme
import iris.ui.theme.rememberBackgroundImage
/**
* Root composable shared by the Android and Desktop shells.
*
* M1: routes between the Connect screen (unpaired / auth failed) and the
* Chat screen (paired). Later milestones add the channel list, search,
* settings, and media (docs/10-android-app.md).
*/
@Composable
fun IrisApp(
store: SecureStore,
deepLinkChatId: String? = null,
deepLinkThreadId: String? = null,
) {
val controller = remember(store) { IrisController(store) }
DisposableEffect(controller) {
setActiveController(controller)
onDispose {
setActiveController(null)
controller.dispose()
}
}
// M5: open the chat referenced by a push notification tap (applied now or
// on the next hello.ack, whichever is later).
LaunchedEffect(deepLinkChatId, deepLinkThreadId) {
if (!deepLinkChatId.isNullOrBlank()) {
controller.openDeepLink(deepLinkChatId, deepLinkThreadId)
}
}
val state by controller.client.state.collectAsState()
val theme by controller.theme.collectAsState()
IrisTheme {
CompositionLocalProvider(LocalUserTheme provides theme) {
// The color goes through Surface's `color` parameter: a
// Modifier.background on the Surface would be painted *under* the
// Surface's own colorScheme.surface fill and never show through.
// contentColor is pinned to the app's light text color — Surface
// would otherwise derive it from the user's background (black
// text on a light background), which would also darken text that
// sits on the app's own dark panels.
Surface(
modifier = Modifier.fillMaxSize(),
color = theme.background,
contentColor = IrisColors.onBackground,
) {
val bgImage = rememberBackgroundImage(theme)
Box(modifier = Modifier.fillMaxSize()) {
val bmp = bgImage
if (bmp != null) {
Image(
bitmap = bmp,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
}
val s = state
when (s) {
GatewayClient.State.Disconnected ->
ConnectScreen(controller, prefillUrl = store.serverUrl, prefillToken = store.token)
is GatewayClient.State.AuthFailed ->
ConnectScreen(
controller,
prefillUrl = store.serverUrl,
prefillToken = store.token,
initialError = "Pairing rejected: ${s.message}",
)
// Connecting / Reconnecting / Connected all render the chat; the header
// status bubble + connection banner show the link state
// without blocking the view (M7's full-screen spinner is gone).
else -> ChatScreen(controller)
}
// M9: full-screen HTML artifact preview (opened from an
// artifact card in the chat); covers everything while open.
ArtifactPreviewOverlay()
}
}
}
}
}