107 lines
4.4 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |