User theme: customizable bubble colors + background (color/image) in Settings → Appearance
- UserTheme model (ARGB ints) + LocalUserTheme, persisted via SecureStore - HSV color picker dialog (Apply keeps open, OK applies+closes, Cancel) - Background: solid color or image (SAF on Android, JFileChooser on desktop) - Reasoning block: 0.6 black overlay on agent bubble color (adapts automatically) - Contrast-aware text on bubbles + pinned contentColor on root Surface
This commit is contained in:
1 parent
86a4c8ee70
commit
0e8f69a43c
11 files changed
+650
-30
No files matched your search
@@ -1,14 +1,18 @@
|
||||
package iris
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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
|
||||
@@ -17,6 +21,8 @@ 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.
|
||||
@@ -47,24 +53,50 @@ fun IrisApp(
|
||||
}
|
||||
}
|
||||
val state by controller.client.state.collectAsState()
|
||||
val theme by controller.theme.collectAsState()
|
||||
|
||||
IrisTheme {
|
||||
Surface(modifier = Modifier.background(IrisColors.background)) {
|
||||
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
|
||||
// StatusChip + connection banner show the link state without
|
||||
// blocking the view (M7's full-screen spinner is gone).
|
||||
else -> ChatScreen(controller)
|
||||
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
|
||||
// StatusChip + connection banner show the link state without
|
||||
// blocking the view (M7's full-screen spinner is gone).
|
||||
else -> ChatScreen(controller)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user