Skip to content

Commit 511b7ea

Browse files
authored
Merge cbea63f into 57e0bda
2 parents 57e0bda + cbea63f commit 511b7ea

13 files changed

Lines changed: 244 additions & 59 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package ai.openclaw.app
2+
3+
/** User-selectable app theme mode for Android appearance settings. */
4+
enum class AppearanceThemeMode(
5+
val rawValue: String,
6+
val displayLabel: String,
7+
) {
8+
System(rawValue = "system", displayLabel = "System"),
9+
Dark(rawValue = "dark", displayLabel = "Dark"),
10+
Light(rawValue = "light", displayLabel = "Light"),
11+
;
12+
13+
fun isDark(systemDark: Boolean): Boolean =
14+
when (this) {
15+
System -> systemDark
16+
Dark -> true
17+
Light -> false
18+
}
19+
20+
companion object {
21+
fun fromRawValue(value: String?): AppearanceThemeMode = entries.firstOrNull { it.rawValue == value?.trim()?.lowercase() } ?: Dark
22+
23+
fun fromDisplayLabel(label: String): AppearanceThemeMode = entries.firstOrNull { it.displayLabel.equals(label.trim(), ignoreCase = true) } ?: Dark
24+
}
25+
}

apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.material3.Surface
1414
import androidx.compose.material3.Text
1515
import androidx.compose.runtime.Composable
1616
import androidx.compose.runtime.LaunchedEffect
17+
import androidx.compose.runtime.collectAsState
1718
import androidx.compose.runtime.getValue
1819
import androidx.compose.runtime.mutableStateOf
1920
import androidx.compose.runtime.remember
@@ -64,8 +65,16 @@ class MainActivity : ComponentActivity() {
6465
activeViewModel = readyViewModel
6566
}
6667

67-
OpenClawTheme {
68-
activeViewModel?.let { RootScreen(viewModel = it) } ?: StartupSurface()
68+
val currentViewModel = activeViewModel
69+
if (currentViewModel == null) {
70+
OpenClawTheme {
71+
StartupSurface()
72+
}
73+
} else {
74+
val appearanceThemeMode by currentViewModel.appearanceThemeMode.collectAsState()
75+
OpenClawTheme(themeMode = appearanceThemeMode) {
76+
RootScreen(viewModel = currentViewModel)
77+
}
6978
}
7079
}
7180
}

apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class MainViewModel(
172172
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
173173
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
174174
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
175+
val appearanceThemeMode: StateFlow<AppearanceThemeMode> = prefs.appearanceThemeMode
175176
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = runtimeState(initial = VoiceCaptureMode.Off) { it.voiceCaptureMode }
176177
val micEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.micEnabled }
177178

@@ -440,6 +441,10 @@ class MainViewModel(
440441
ensureRuntime().setSpeakerEnabled(enabled)
441442
}
442443

444+
fun setAppearanceThemeMode(mode: AppearanceThemeMode) {
445+
prefs.setAppearanceThemeMode(mode)
446+
}
447+
443448
fun refreshGatewayConnection() {
444449
viewModelScope.launch(Dispatchers.Default) {
445450
ensureRuntime().refreshGatewayConnection()

apps/android/app/src/main/java/ai/openclaw/app/SecurePrefs.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class SecurePrefs(
4242
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
4343
private const val installedAppsSharingEnabledKey = "device.apps.sharing.enabled"
4444
private const val voiceMicEnabledKey = "voice.micEnabled"
45+
private const val appearanceThemeModeKey = "appearance.themeMode"
4546
}
4647

4748
private val appContext = context.applicationContext
@@ -181,6 +182,10 @@ class SecurePrefs(
181182
private val _speakerEnabled = MutableStateFlow(plainPrefs.getBoolean("voice.speakerEnabled", true))
182183
val speakerEnabled: StateFlow<Boolean> = _speakerEnabled
183184

185+
private val _appearanceThemeMode =
186+
MutableStateFlow(AppearanceThemeMode.fromRawValue(plainPrefs.getString(appearanceThemeModeKey, null)))
187+
val appearanceThemeMode: StateFlow<AppearanceThemeMode> = _appearanceThemeMode
188+
184189
fun setLastDiscoveredStableId(value: String) {
185190
val trimmed = value.trim()
186191
plainPrefs.edit { putString("gateway.lastDiscoveredStableID", trimmed) }
@@ -525,6 +530,11 @@ class SecurePrefs(
525530
_speakerEnabled.value = value
526531
}
527532

533+
fun setAppearanceThemeMode(mode: AppearanceThemeMode) {
534+
plainPrefs.edit { putString(appearanceThemeModeKey, mode.rawValue) }
535+
_appearanceThemeMode.value = mode
536+
}
537+
528538
private fun loadNotificationForwardingPackages(): Set<String> {
529539
val raw = plainPrefs.getString(notificationsForwardingPackagesKey, null)?.trim()
530540
if (raw.isNullOrEmpty()) {

apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,27 @@ internal data class MobileColors(
4141

4242
internal fun lightMobileColors() =
4343
MobileColors(
44-
surface = Color(0xFFF6F7FA),
45-
surfaceStrong = Color(0xFFECEEF3),
44+
surface = Color(0xFFFAFBFC),
45+
surfaceStrong = Color(0xFFEFF3F8),
4646
cardSurface = Color(0xFFFFFFFF),
47-
border = Color(0xFFE5E7EC),
48-
borderStrong = Color(0xFFD6DAE2),
49-
text = Color(0xFF17181C),
50-
textSecondary = Color(0xFF5D6472),
51-
textTertiary = Color(0xFF99A0AE),
52-
accent = Color(0xFF1D5DD8),
53-
accentSoft = Color(0xFFECF3FF),
54-
accentBorderStrong = Color(0xFF184DAF),
55-
success = Color(0xFF2F8C5A),
56-
successSoft = Color(0xFFEEF9F3),
57-
warning = Color(0xFFC8841A),
58-
warningSoft = Color(0xFFFFF8EC),
59-
danger = Color(0xFFD04B4B),
60-
dangerSoft = Color(0xFFFFF2F2),
61-
codeBg = Color(0xFF15171B),
62-
codeText = Color(0xFFE8EAEE),
63-
codeBorder = Color(0xFF2B2E35),
64-
codeAccent = Color(0xFF3FC97A),
47+
border = Color(0xFFDDE3EC),
48+
borderStrong = Color(0xFFC7D0DC),
49+
text = Color(0xFF16181D),
50+
textSecondary = Color(0xFF505B6A),
51+
textTertiary = Color(0xFF8E98A7),
52+
accent = Color(0xFF1B5ACB),
53+
accentSoft = Color(0xFFEAF2FF),
54+
accentBorderStrong = Color(0xFF174CA9),
55+
success = Color(0xFF287F52),
56+
successSoft = Color(0xFFEAF7F0),
57+
warning = Color(0xFFAF7418),
58+
warningSoft = Color(0xFFFFF4DF),
59+
danger = Color(0xFFC94343),
60+
dangerSoft = Color(0xFFFFECEC),
61+
codeBg = Color(0xFFEFF3F8),
62+
codeText = Color(0xFF172033),
63+
codeBorder = Color(0xFFD7DDE7),
64+
codeAccent = Color(0xFF287F52),
6565
chipBorderConnected = Color(0xFFCFEBD8),
6666
chipBorderConnecting = Color(0xFFD5E2FA),
6767
chipBorderWarning = Color(0xFFEED8B8),

apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.compose.foundation.BorderStroke
3434
import androidx.compose.foundation.Canvas
3535
import androidx.compose.foundation.Image
3636
import androidx.compose.foundation.background
37+
import androidx.compose.foundation.isSystemInDarkTheme
3738
import androidx.compose.foundation.layout.Arrangement
3839
import androidx.compose.foundation.layout.Box
3940
import androidx.compose.foundation.layout.Column
@@ -130,7 +131,9 @@ fun OnboardingFlow(
130131
viewModel: MainViewModel,
131132
modifier: Modifier = Modifier,
132133
) {
133-
ClawDesignTheme {
134+
val appearanceThemeMode by viewModel.appearanceThemeMode.collectAsState()
135+
val onboardingDark = appearanceThemeMode.isDark(systemDark = isSystemInDarkTheme())
136+
ClawDesignTheme(dark = onboardingDark) {
134137
val context = LocalContext.current
135138
val statusText by viewModel.statusText.collectAsState()
136139
val gatewayConnectionProblem by viewModel.gatewayConnectionProblem.collectAsState()
@@ -159,6 +162,8 @@ fun OnboardingFlow(
159162
var connectAttemptStartedAtMs by rememberSaveable { mutableLongStateOf(0L) }
160163
var recoveryNowMs by remember { mutableLongStateOf(SystemClock.elapsedRealtime()) }
161164

165+
OpenClawSystemBarAppearance(lightAppearance = !onboardingDark && step != OnboardingStep.Welcome)
166+
162167
val qrScannerOptions =
163168
remember {
164169
GmsBarcodeScannerOptions
@@ -223,10 +228,12 @@ fun OnboardingFlow(
223228

224229
when (step) {
225230
OnboardingStep.Welcome ->
226-
WelcomeScreen(
227-
modifier = modifier,
228-
onConnect = { step = OnboardingStep.Gateway },
229-
)
231+
ClawDesignTheme(dark = true) {
232+
WelcomeScreen(
233+
modifier = modifier,
234+
onConnect = { step = OnboardingStep.Gateway },
235+
)
236+
}
230237
OnboardingStep.Gateway ->
231238
GatewaySetupScreen(
232239
modifier = modifier,

apps/android/app/src/main/java/ai/openclaw/app/ui/OpenClawTheme.kt

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ai.openclaw.app.ui
22

3+
import ai.openclaw.app.AppearanceThemeMode
34
import android.app.Activity
45
import androidx.compose.foundation.isSystemInDarkTheme
56
import androidx.compose.material3.MaterialTheme
@@ -8,34 +9,51 @@ import androidx.compose.material3.dynamicLightColorScheme
89
import androidx.compose.runtime.Composable
910
import androidx.compose.runtime.CompositionLocalProvider
1011
import androidx.compose.runtime.SideEffect
12+
import androidx.compose.runtime.staticCompositionLocalOf
1113
import androidx.compose.ui.graphics.Color
1214
import androidx.compose.ui.platform.LocalContext
1315
import androidx.compose.ui.platform.LocalView
1416
import androidx.core.view.WindowCompat
1517

18+
private val LocalOpenClawDarkTheme = staticCompositionLocalOf { true }
19+
1620
/**
1721
* App theme wrapper that installs dynamic Material colors and legacy mobile color tokens.
1822
*/
1923
@Composable
20-
fun OpenClawTheme(content: @Composable () -> Unit) {
24+
fun OpenClawTheme(
25+
themeMode: AppearanceThemeMode = AppearanceThemeMode.Dark,
26+
content: @Composable () -> Unit,
27+
) {
2128
val context = LocalContext.current
22-
val isDark = isSystemInDarkTheme()
29+
val isDark = themeMode.isDark(systemDark = isSystemInDarkTheme())
2330
val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
2431
val mobileColors = if (isDark) darkMobileColors() else lightMobileColors()
2532

33+
OpenClawSystemBarAppearance(lightAppearance = !isDark)
34+
35+
CompositionLocalProvider(
36+
LocalMobileColors provides mobileColors,
37+
LocalOpenClawDarkTheme provides isDark,
38+
) {
39+
MaterialTheme(colorScheme = colorScheme, content = content)
40+
}
41+
}
42+
43+
@Composable
44+
internal fun OpenClawSystemBarAppearance(lightAppearance: Boolean) {
2645
val view = LocalView.current
2746
if (!view.isInEditMode) {
2847
SideEffect {
29-
val window = (view.context as Activity).window
48+
val window = (view.context as? Activity)?.window ?: return@SideEffect
49+
WindowCompat
50+
.getInsetsController(window, window.decorView)
51+
.isAppearanceLightStatusBars = lightAppearance
3052
WindowCompat
3153
.getInsetsController(window, window.decorView)
32-
.isAppearanceLightStatusBars = !isDark
54+
.isAppearanceLightNavigationBars = lightAppearance
3355
}
3456
}
35-
36-
CompositionLocalProvider(LocalMobileColors provides mobileColors) {
37-
MaterialTheme(colorScheme = colorScheme, content = content)
38-
}
3957
}
4058

4159
/**
@@ -44,9 +62,9 @@ fun OpenClawTheme(content: @Composable () -> Unit) {
4462
@Composable
4563
fun overlayContainerColor(): Color {
4664
val scheme = MaterialTheme.colorScheme
47-
val isDark = isSystemInDarkTheme()
65+
val isDark = LocalOpenClawDarkTheme.current
4866
val base = if (isDark) scheme.surfaceContainerLow else scheme.surfaceContainerHigh
49-
// Light mode: background stays dark (canvas), so clamp overlays away from pure-white glare.
67+
// Light mode keeps overlays away from pure-white glare on the app canvas.
5068
return if (isDark) base else base.copy(alpha = 0.88f)
5169
}
5270

apps/android/app/src/main/java/ai/openclaw/app/ui/SessionsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ private fun SessionRow(
217217
compact: Boolean,
218218
onClick: () -> Unit,
219219
) {
220-
Surface(onClick = onClick, color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
220+
Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
221221
Column {
222222
Row(
223223
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp).padding(vertical = 5.dp),

apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ai.openclaw.app.ui
22

3+
import ai.openclaw.app.AppearanceThemeMode
34
import ai.openclaw.app.BuildConfig
45
import ai.openclaw.app.GatewayAgentSummary
56
import ai.openclaw.app.GatewayCronJobSummary
@@ -146,7 +147,7 @@ internal fun SettingsDetailScreen(
146147
SettingsRoute.Notifications -> NotificationSettingsScreen(viewModel = viewModel, onBack = onBack)
147148
SettingsRoute.PhoneCapabilities -> PhoneCapabilitiesScreen(viewModel = viewModel, onBack = onBack)
148149
SettingsRoute.Gateway -> GatewaySettingsScreen(viewModel = viewModel, onBack = onBack)
149-
SettingsRoute.Appearance -> AppearanceSettingsScreen(onBack = onBack)
150+
SettingsRoute.Appearance -> AppearanceSettingsScreen(viewModel = viewModel, onBack = onBack)
150151
SettingsRoute.Health -> HealthLogsSettingsScreen(viewModel = viewModel, onBack = onBack)
151152
SettingsRoute.About -> AboutSettingsScreen(viewModel = viewModel, onBack = onBack)
152153
}
@@ -914,18 +915,70 @@ private fun GatewaySettingsScreen(
914915
}
915916

916917
@Composable
917-
private fun AppearanceSettingsScreen(onBack: () -> Unit) {
918+
private fun AppearanceSettingsScreen(
919+
viewModel: MainViewModel,
920+
onBack: () -> Unit,
921+
) {
922+
val themeMode by viewModel.appearanceThemeMode.collectAsState()
923+
918924
SettingsDetailFrame(title = "Appearance", subtitle = "A calm, high-contrast OpenClaw interface.", icon = Icons.Default.Palette, onBack = onBack) {
919925
SettingsMetricPanel(
920926
rows =
921927
listOf(
922-
SettingsMetric("Theme", "Dark"),
928+
SettingsMetric("Theme", appearanceThemeSummary(themeMode)),
923929
SettingsMetric("Contrast", "High"),
924930
SettingsMetric("Typography", "Readable"),
925931
),
926932
)
927933
ClawPanel {
928-
Text(text = "OpenClaw uses a fixed premium dark theme so it stays consistent across devices.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
934+
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
935+
Text(text = "Theme", style = ClawTheme.type.section, color = ClawTheme.colors.text)
936+
ClawSegmentedControl(
937+
options = appearanceThemeOptions(),
938+
selected = appearanceThemeSummary(themeMode),
939+
onSelect = { selected -> viewModel.setAppearanceThemeMode(appearanceThemeModeForLabel(selected)) },
940+
)
941+
}
942+
}
943+
ClawPanel {
944+
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
945+
Text(text = "Palette", style = ClawTheme.type.section, color = ClawTheme.colors.text)
946+
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
947+
AppearancePaletteSwatch("Canvas", ClawTheme.colors.canvas, modifier = Modifier.weight(1f))
948+
AppearancePaletteSwatch("Surface", ClawTheme.colors.surfaceRaised, modifier = Modifier.weight(1f))
949+
AppearancePaletteSwatch(
950+
"Accent",
951+
ClawTheme.colors.primary,
952+
contentColor = ClawTheme.colors.canvas,
953+
modifier = Modifier.weight(1f),
954+
)
955+
}
956+
}
957+
}
958+
}
959+
}
960+
961+
internal fun appearanceThemeSummary(mode: AppearanceThemeMode): String = mode.displayLabel
962+
963+
internal fun appearanceThemeOptions(): List<String> = AppearanceThemeMode.entries.map { it.displayLabel }
964+
965+
internal fun appearanceThemeModeForLabel(label: String): AppearanceThemeMode = AppearanceThemeMode.fromDisplayLabel(label)
966+
967+
@Composable
968+
private fun AppearancePaletteSwatch(
969+
label: String,
970+
color: Color,
971+
contentColor: Color = ClawTheme.colors.text,
972+
modifier: Modifier = Modifier,
973+
) {
974+
Surface(
975+
modifier = modifier.height(58.dp),
976+
shape = RoundedCornerShape(ClawTheme.radii.control),
977+
color = color,
978+
border = BorderStroke(1.dp, ClawTheme.colors.border),
979+
) {
980+
Box(modifier = Modifier.fillMaxSize().padding(8.dp), contentAlignment = Alignment.BottomStart) {
981+
Text(text = label, style = ClawTheme.type.caption, color = contentColor, maxLines = 1)
929982
}
930983
}
931984
}

0 commit comments

Comments
 (0)