style(android): align settings screen with RN visual system
This commit is contained in:
parent
cf031d6ad4
commit
02e3fbef77
@ -10,9 +10,13 @@ import android.provider.Settings
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -23,6 +27,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@ -30,16 +35,20 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -56,8 +65,12 @@ import androidx.compose.ui.draw.alpha
|
|||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import ai.openclaw.android.BuildConfig
|
import ai.openclaw.android.BuildConfig
|
||||||
@ -114,6 +127,14 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
versionName
|
versionName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val listItemColors =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
headlineColor = mobileText,
|
||||||
|
supportingColor = mobileTextSecondary,
|
||||||
|
trailingIconColor = mobileTextSecondary,
|
||||||
|
leadingIconColor = mobileTextSecondary,
|
||||||
|
)
|
||||||
|
|
||||||
if (pendingTrust != null) {
|
if (pendingTrust != null) {
|
||||||
val prompt = pendingTrust!!
|
val prompt = pendingTrust!!
|
||||||
@ -284,41 +305,78 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
"Discovery active • ${visibleGateways.size} gateway${if (visibleGateways.size == 1) "" else "s"} found"
|
"Discovery active • ${visibleGateways.size} gateway${if (visibleGateways.size == 1) "" else "s"} found"
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
Box(
|
||||||
state = listState,
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.fillMaxHeight()
|
.background(mobileBackgroundGradient),
|
||||||
.imePadding()
|
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.imePadding()
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)),
|
||||||
|
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
Text(
|
||||||
|
"SETTINGS",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
Text("Device + Gateway Configuration", style = mobileTitle2, color = mobileText)
|
||||||
|
Text(
|
||||||
|
"Manage capabilities, connection mode, permissions, and diagnostics.",
|
||||||
|
style = mobileCallout,
|
||||||
|
color = mobileTextSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Order parity: Node → Gateway → Voice → Camera → Messaging → Location → Screen.
|
// Order parity: Node → Gateway → Voice → Camera → Messaging → Location → Screen.
|
||||||
item { Text("Node", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
|
Text(
|
||||||
|
"NODE",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = displayName,
|
value = displayName,
|
||||||
onValueChange = viewModel::setDisplayName,
|
onValueChange = viewModel::setDisplayName,
|
||||||
label = { Text("Name") },
|
label = { Text("Name", style = mobileCaption1, color = mobileTextSecondary) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textStyle = mobileBody.copy(color = mobileText),
|
||||||
|
colors = settingsTextFieldColors(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { Text("Instance ID: $instanceId", color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
item { Text("Instance ID: $instanceId", style = mobileCallout.copy(fontFamily = FontFamily.Monospace), color = mobileTextSecondary) }
|
||||||
item { Text("Device: $deviceModel", color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
item { Text("Device: $deviceModel", style = mobileCallout, color = mobileTextSecondary) }
|
||||||
item { Text("Version: $appVersion", color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
item { Text("Version: $appVersion", style = mobileCallout, color = mobileTextSecondary) }
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Gateway
|
// Gateway
|
||||||
item { Text("Gateway", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
item { ListItem(headlineContent = { Text("Status") }, supportingContent = { Text(statusText) }) }
|
Text(
|
||||||
|
"GATEWAY",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { ListItem(modifier = settingsRowModifier(), colors = listItemColors, headlineContent = { Text("Status", style = mobileHeadline) }, supportingContent = { Text(statusText, style = mobileCallout) }) }
|
||||||
if (serverName != null) {
|
if (serverName != null) {
|
||||||
item { ListItem(headlineContent = { Text("Server") }, supportingContent = { Text(serverName!!) }) }
|
item { ListItem(modifier = settingsRowModifier(), colors = listItemColors, headlineContent = { Text("Server", style = mobileHeadline) }, supportingContent = { Text(serverName!!, style = mobileCallout) }) }
|
||||||
}
|
}
|
||||||
if (remoteAddress != null) {
|
if (remoteAddress != null) {
|
||||||
item { ListItem(headlineContent = { Text("Address") }, supportingContent = { Text(remoteAddress!!) }) }
|
item { ListItem(modifier = settingsRowModifier(), colors = listItemColors, headlineContent = { Text("Address", style = mobileHeadline) }, supportingContent = { Text(remoteAddress!!, style = mobileCallout.copy(fontFamily = FontFamily.Monospace)) }) }
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
// UI sanity: "Disconnect" only when we have an active remote.
|
// UI sanity: "Disconnect" only when we have an active remote.
|
||||||
@ -328,23 +386,26 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
viewModel.disconnect()
|
viewModel.disconnect()
|
||||||
NodeForegroundService.stop(context)
|
NodeForegroundService.stop(context)
|
||||||
},
|
},
|
||||||
|
colors = settingsDangerButtonColors(),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
) {
|
) {
|
||||||
Text("Disconnect")
|
Text("Disconnect", style = mobileHeadline.copy(fontWeight = FontWeight.Bold))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
if (!isConnected || visibleGateways.isNotEmpty()) {
|
if (!isConnected || visibleGateways.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
if (isConnected) "Other Gateways" else "Discovered Gateways",
|
if (isConnected) "Other Gateways" else "Discovered Gateways",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = mobileHeadline,
|
||||||
|
color = mobileText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!isConnected && visibleGateways.isEmpty()) {
|
if (!isConnected && visibleGateways.isEmpty()) {
|
||||||
item { Text("No gateways found yet.", color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
item { Text("No gateways found yet.", style = mobileCallout, color = mobileTextSecondary) }
|
||||||
} else {
|
} else {
|
||||||
items(items = visibleGateways, key = { it.stableId }) { gateway ->
|
items(items = visibleGateways, key = { it.stableId }) { gateway ->
|
||||||
val detailLines =
|
val detailLines =
|
||||||
@ -359,11 +420,13 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(gateway.name) },
|
modifier = settingsRowModifier(),
|
||||||
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text(gateway.name, style = mobileHeadline) },
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
detailLines.forEach { line ->
|
detailLines.forEach { line ->
|
||||||
Text(line, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(line, style = mobileCallout, color = mobileTextSecondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -373,8 +436,10 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
NodeForegroundService.start(context)
|
NodeForegroundService.start(context)
|
||||||
viewModel.connect(gateway)
|
viewModel.connect(gateway)
|
||||||
},
|
},
|
||||||
|
colors = settingsPrimaryButtonColors(),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
) {
|
) {
|
||||||
Text("Connect")
|
Text("Connect", style = mobileCallout.copy(fontWeight = FontWeight.Bold))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -385,66 +450,82 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
gatewayDiscoveryFooterText,
|
gatewayDiscoveryFooterText,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = mobileCaption1,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = mobileTextSecondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Advanced") },
|
modifier = settingsRowModifier().then(Modifier.clickable { setAdvancedExpanded(!advancedExpanded) }),
|
||||||
supportingContent = { Text("Manual gateway connection") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Advanced", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Manual gateway connection", style = mobileCallout) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (advancedExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
imageVector = if (advancedExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||||
contentDescription = if (advancedExpanded) "Collapse" else "Expand",
|
contentDescription = if (advancedExpanded) "Collapse" else "Expand",
|
||||||
|
tint = mobileTextSecondary,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
|
||||||
Modifier.clickable {
|
|
||||||
setAdvancedExpanded(!advancedExpanded)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
AnimatedVisibility(visible = advancedExpanded) {
|
AnimatedVisibility(visible = advancedExpanded) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(width = 1.dp, color = mobileBorder, shape = RoundedCornerShape(14.dp))
|
||||||
|
.background(mobileSurface, RoundedCornerShape(14.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Use Manual Gateway") },
|
modifier = settingsRowModifier(),
|
||||||
supportingContent = { Text("Use this when discovery is blocked.") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Use Manual Gateway", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Use this when discovery is blocked.", style = mobileCallout) },
|
||||||
trailingContent = { Switch(checked = manualEnabled, onCheckedChange = viewModel::setManualEnabled) },
|
trailingContent = { Switch(checked = manualEnabled, onCheckedChange = viewModel::setManualEnabled) },
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = manualHost,
|
value = manualHost,
|
||||||
onValueChange = viewModel::setManualHost,
|
onValueChange = viewModel::setManualHost,
|
||||||
label = { Text("Host") },
|
label = { Text("Host", style = mobileCaption1, color = mobileTextSecondary) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = manualEnabled,
|
enabled = manualEnabled,
|
||||||
|
textStyle = mobileBody.copy(color = mobileText),
|
||||||
|
colors = settingsTextFieldColors(),
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = manualPort.toString(),
|
value = manualPort.toString(),
|
||||||
onValueChange = { v -> viewModel.setManualPort(v.toIntOrNull() ?: 0) },
|
onValueChange = { v -> viewModel.setManualPort(v.toIntOrNull() ?: 0) },
|
||||||
label = { Text("Port") },
|
label = { Text("Port", style = mobileCaption1, color = mobileTextSecondary) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = manualEnabled,
|
enabled = manualEnabled,
|
||||||
|
textStyle = mobileBody.copy(fontFamily = FontFamily.Monospace, color = mobileText),
|
||||||
|
colors = settingsTextFieldColors(),
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = gatewayToken,
|
value = gatewayToken,
|
||||||
onValueChange = viewModel::setGatewayToken,
|
onValueChange = viewModel::setGatewayToken,
|
||||||
label = { Text("Gateway Token") },
|
label = { Text("Gateway Token", style = mobileCaption1, color = mobileTextSecondary) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = manualEnabled,
|
enabled = manualEnabled,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
textStyle = mobileBody.copy(color = mobileText),
|
||||||
|
colors = settingsTextFieldColors(),
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Require TLS") },
|
modifier = settingsRowModifier().alpha(if (manualEnabled) 1f else 0.5f),
|
||||||
supportingContent = { Text("Pin the gateway certificate on first connect.") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Require TLS", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Pin the gateway certificate on first connect.", style = mobileCallout) },
|
||||||
trailingContent = { Switch(checked = manualTls, onCheckedChange = viewModel::setManualTls, enabled = manualEnabled) },
|
trailingContent = { Switch(checked = manualTls, onCheckedChange = viewModel::setManualTls, enabled = manualEnabled) },
|
||||||
modifier = Modifier.alpha(if (manualEnabled) 1f else 0.5f),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val hostOk = manualHost.trim().isNotEmpty()
|
val hostOk = manualHost.trim().isNotEmpty()
|
||||||
@ -455,26 +536,36 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
viewModel.connectManual()
|
viewModel.connectManual()
|
||||||
},
|
},
|
||||||
enabled = manualEnabled && hostOk && portOk,
|
enabled = manualEnabled && hostOk && portOk,
|
||||||
|
colors = settingsPrimaryButtonColors(),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
) {
|
) {
|
||||||
Text("Connect (Manual)")
|
Text("Connect (Manual)", style = mobileCallout.copy(fontWeight = FontWeight.Bold))
|
||||||
}
|
}
|
||||||
|
|
||||||
TextButton(onClick = { viewModel.setOnboardingCompleted(false) }) {
|
TextButton(onClick = { viewModel.setOnboardingCompleted(false) }) {
|
||||||
Text("Run onboarding again")
|
Text("Run onboarding again", style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), color = mobileAccent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Voice
|
// Voice
|
||||||
item { Text("Voice", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
|
Text(
|
||||||
|
"VOICE",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
val enabled = voiceWakeMode != VoiceWakeMode.Off
|
val enabled = voiceWakeMode != VoiceWakeMode.Off
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Voice Wake") },
|
modifier = settingsRowModifier(),
|
||||||
supportingContent = { Text(voiceWakeStatusText) },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Voice Wake", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text(voiceWakeStatusText, style = mobileCallout) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = enabled,
|
checked = enabled,
|
||||||
@ -497,8 +588,10 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
AnimatedVisibility(visible = voiceWakeMode != VoiceWakeMode.Off) {
|
AnimatedVisibility(visible = voiceWakeMode != VoiceWakeMode.Off) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) {
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Foreground Only") },
|
modifier = settingsRowModifier(),
|
||||||
supportingContent = { Text("Listens only while OpenClaw is open.") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Foreground Only", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Listens only while OpenClaw is open.", style = mobileCallout) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = voiceWakeMode == VoiceWakeMode.Foreground,
|
selected = voiceWakeMode == VoiceWakeMode.Foreground,
|
||||||
@ -513,8 +606,10 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Always") },
|
modifier = settingsRowModifier(),
|
||||||
supportingContent = { Text("Keeps listening in the background (shows a persistent notification).") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Always", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Keeps listening in the background (shows a persistent notification).", style = mobileCallout) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = voiceWakeMode == VoiceWakeMode.Always,
|
selected = voiceWakeMode == VoiceWakeMode.Always,
|
||||||
@ -535,7 +630,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = wakeWordsText,
|
value = wakeWordsText,
|
||||||
onValueChange = setWakeWordsText,
|
onValueChange = setWakeWordsText,
|
||||||
label = { Text("Wake Words (comma-separated)") },
|
label = { Text("Wake Words (comma-separated)", style = mobileCaption1, color = mobileTextSecondary) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth().onFocusChanged { focusState ->
|
Modifier.fillMaxWidth().onFocusChanged { focusState ->
|
||||||
if (focusState.isFocused) {
|
if (focusState.isFocused) {
|
||||||
@ -554,9 +649,19 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
textStyle = mobileBody.copy(color = mobileText),
|
||||||
|
colors = settingsTextFieldColors(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { Button(onClick = viewModel::resetWakeWordsDefaults) { Text("Reset defaults") } }
|
item {
|
||||||
|
Button(
|
||||||
|
onClick = viewModel::resetWakeWordsDefaults,
|
||||||
|
colors = settingsPrimaryButtonColors(),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
) {
|
||||||
|
Text("Reset defaults", style = mobileCallout.copy(fontWeight = FontWeight.Bold))
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
@ -564,32 +669,48 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
} else {
|
} else {
|
||||||
"Connect to a gateway to sync wake words globally."
|
"Connect to a gateway to sync wake words globally."
|
||||||
},
|
},
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
style = mobileCallout,
|
||||||
|
color = mobileTextSecondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
item { Text("Camera", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
|
Text(
|
||||||
|
"CAMERA",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Allow Camera") },
|
modifier = settingsRowModifier(),
|
||||||
supportingContent = { Text("Allows the gateway to request photos or short video clips (foreground only).") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Allow Camera", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Allows the gateway to request photos or short video clips (foreground only).", style = mobileCallout) },
|
||||||
trailingContent = { Switch(checked = cameraEnabled, onCheckedChange = ::setCameraEnabledChecked) },
|
trailingContent = { Switch(checked = cameraEnabled, onCheckedChange = ::setCameraEnabledChecked) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
"Tip: grant Microphone permission for video clips with audio.",
|
"Tip: grant Microphone permission for video clips with audio.",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
style = mobileCallout,
|
||||||
|
color = mobileTextSecondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Messaging
|
// Messaging
|
||||||
item { Text("Messaging", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
|
Text(
|
||||||
|
"MESSAGING",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
val buttonLabel =
|
val buttonLabel =
|
||||||
when {
|
when {
|
||||||
@ -598,7 +719,9 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
else -> "Grant"
|
else -> "Grant"
|
||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("SMS Permission") },
|
modifier = settingsRowModifier(),
|
||||||
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("SMS Permission", style = mobileHeadline) },
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
if (smsPermissionAvailable) {
|
if (smsPermissionAvailable) {
|
||||||
@ -606,6 +729,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
} else {
|
} else {
|
||||||
"SMS requires a device with telephony hardware."
|
"SMS requires a device with telephony hardware."
|
||||||
},
|
},
|
||||||
|
style = mobileCallout,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
@ -619,91 +743,125 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = smsPermissionAvailable,
|
enabled = smsPermissionAvailable,
|
||||||
|
colors = settingsPrimaryButtonColors(),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
) {
|
) {
|
||||||
Text(buttonLabel)
|
Text(buttonLabel, style = mobileCallout.copy(fontWeight = FontWeight.Bold))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
item { Text("Location", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
item {
|
Text(
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) {
|
"LOCATION",
|
||||||
ListItem(
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
headlineContent = { Text("Off") },
|
color = mobileAccent,
|
||||||
supportingContent = { Text("Disable location sharing.") },
|
|
||||||
trailingContent = {
|
|
||||||
RadioButton(
|
|
||||||
selected = locationMode == LocationMode.Off,
|
|
||||||
onClick = { viewModel.setLocationMode(LocationMode.Off) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text("While Using") },
|
|
||||||
supportingContent = { Text("Only while OpenClaw is open.") },
|
|
||||||
trailingContent = {
|
|
||||||
RadioButton(
|
|
||||||
selected = locationMode == LocationMode.WhileUsing,
|
|
||||||
onClick = { requestLocationPermissions(LocationMode.WhileUsing) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text("Always") },
|
|
||||||
supportingContent = { Text("Allow background location (requires system permission).") },
|
|
||||||
trailingContent = {
|
|
||||||
RadioButton(
|
|
||||||
selected = locationMode == LocationMode.Always,
|
|
||||||
onClick = { requestLocationPermissions(LocationMode.Always) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
item {
|
||||||
item {
|
Column(modifier = settingsRowModifier(), verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Precise Location") },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
supportingContent = { Text("Use precise GPS when available.") },
|
colors = listItemColors,
|
||||||
trailingContent = {
|
headlineContent = { Text("Off", style = mobileHeadline) },
|
||||||
Switch(
|
supportingContent = { Text("Disable location sharing.", style = mobileCallout) },
|
||||||
checked = locationPreciseEnabled,
|
trailingContent = {
|
||||||
onCheckedChange = ::setPreciseLocationChecked,
|
RadioButton(
|
||||||
enabled = locationMode != LocationMode.Off,
|
selected = locationMode == LocationMode.Off,
|
||||||
|
onClick = { viewModel.setLocationMode(LocationMode.Off) },
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
HorizontalDivider(color = mobileBorder)
|
||||||
)
|
ListItem(
|
||||||
}
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("While Using", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Only while OpenClaw is open.", style = mobileCallout) },
|
||||||
|
trailingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = locationMode == LocationMode.WhileUsing,
|
||||||
|
onClick = { requestLocationPermissions(LocationMode.WhileUsing) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = mobileBorder)
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Always", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Allow background location (requires system permission).", style = mobileCallout) },
|
||||||
|
trailingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = locationMode == LocationMode.Always,
|
||||||
|
onClick = { requestLocationPermissions(LocationMode.Always) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = mobileBorder)
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Precise Location", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Use precise GPS when available.", style = mobileCallout) },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = locationPreciseEnabled,
|
||||||
|
onCheckedChange = ::setPreciseLocationChecked,
|
||||||
|
enabled = locationMode != LocationMode.Off,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
"Always may require Android Settings to allow background location.",
|
"Always may require Android Settings to allow background location.",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
style = mobileCallout,
|
||||||
|
color = mobileTextSecondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Screen
|
// Screen
|
||||||
item { Text("Screen", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
|
Text(
|
||||||
|
"SCREEN",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Prevent Sleep") },
|
modifier = settingsRowModifier(),
|
||||||
supportingContent = { Text("Keeps the screen awake while OpenClaw is open.") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Prevent Sleep", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Keeps the screen awake while OpenClaw is open.", style = mobileCallout) },
|
||||||
trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) },
|
trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider(color = mobileBorder) }
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
item { Text("Debug", style = MaterialTheme.typography.titleSmall) }
|
item {
|
||||||
|
Text(
|
||||||
|
"DEBUG",
|
||||||
|
style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
|
||||||
|
color = mobileAccent,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Debug Canvas Status") },
|
modifier = settingsRowModifier(),
|
||||||
supportingContent = { Text("Show status text in the canvas when debug is enabled.") },
|
colors = listItemColors,
|
||||||
|
headlineContent = { Text("Debug Canvas Status", style = mobileHeadline) },
|
||||||
|
supportingContent = { Text("Show status text in the canvas when debug is enabled.", style = mobileCallout) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = canvasDebugStatusEnabled,
|
checked = canvasDebugStatusEnabled,
|
||||||
@ -713,10 +871,47 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(modifier = Modifier.height(20.dp)) }
|
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun settingsTextFieldColors() =
|
||||||
|
OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = mobileSurface,
|
||||||
|
unfocusedContainerColor = mobileSurface,
|
||||||
|
focusedBorderColor = mobileAccent,
|
||||||
|
unfocusedBorderColor = mobileBorder,
|
||||||
|
focusedTextColor = mobileText,
|
||||||
|
unfocusedTextColor = mobileText,
|
||||||
|
cursorColor = mobileAccent,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun settingsRowModifier() =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(width = 1.dp, color = mobileBorder, shape = RoundedCornerShape(14.dp))
|
||||||
|
.background(Color.White, RoundedCornerShape(14.dp))
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun settingsPrimaryButtonColors() =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = mobileAccent,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = mobileAccent.copy(alpha = 0.45f),
|
||||||
|
disabledContentColor = Color.White.copy(alpha = 0.9f),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun settingsDangerButtonColors() =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = mobileDanger,
|
||||||
|
contentColor = Color.White,
|
||||||
|
disabledContainerColor = mobileDanger.copy(alpha = 0.45f),
|
||||||
|
disabledContentColor = Color.White.copy(alpha = 0.9f),
|
||||||
|
)
|
||||||
|
|
||||||
private fun openAppSettings(context: Context) {
|
private fun openAppSettings(context: Context) {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(
|
Intent(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user