feat(tergent): mirror DevicesScreen, DiagnosticsScreen, NotificationsScreen to app/src/main/java
This commit is contained in:
@@ -0,0 +1,504 @@
|
|||||||
|
package ai.openclaw.app.ui.tergent
|
||||||
|
|
||||||
|
import ai.openclaw.app.ui.design.ClawPanel
|
||||||
|
import ai.openclaw.app.ui.design.ClawScaffold
|
||||||
|
import ai.openclaw.app.ui.design.ClawSeparatedColumn
|
||||||
|
import ai.openclaw.app.ui.design.ClawTheme
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
|
import androidx.compose.material.icons.filled.Devices
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tergent color tokens (used in all three screens)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
private val bg = Color(0xFF111827)
|
||||||
|
private val surface = Color(0xFF1F2937)
|
||||||
|
private val border = Color(0xFF374151)
|
||||||
|
private val text = Color(0xFFF9FAFB)
|
||||||
|
private val text2 = Color(0xFF9CA3AF)
|
||||||
|
private val text3 = Color(0xFF6B7280)
|
||||||
|
private val primary = Color(0xFF5B8CFF)
|
||||||
|
private val success = Color(0xFF34D399)
|
||||||
|
private val danger = Color(0xFFF87171)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Data models
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
internal data class PairedDevice(
|
||||||
|
val name: String,
|
||||||
|
val status: String,
|
||||||
|
val port: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class DiscoveredGateway(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DevicesScreen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
internal fun DevicesScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
pairedDevices: List<PairedDevice> = rememberPairedDevices(),
|
||||||
|
discoveredGateways: List<DiscoveredGateway> = rememberDiscoveredGateways(),
|
||||||
|
onAddManualGateway: () -> Unit = {},
|
||||||
|
onDeviceClick: (PairedDevice) -> Unit = {},
|
||||||
|
onGatewayClick: (DiscoveredGateway) -> Unit = {},
|
||||||
|
) {
|
||||||
|
TergentScaffold {
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp),
|
||||||
|
) {
|
||||||
|
// Top bar
|
||||||
|
item {
|
||||||
|
TergentTopBar(title = "设备", onBack = onBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paired gateways section
|
||||||
|
if (pairedDevices.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
TergentSectionTitle(title = "已配对网关")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PairedDevicesPanel(
|
||||||
|
devices = pairedDevices,
|
||||||
|
onDeviceClick = onDeviceClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
TergentSectionTitle(title = "已配对网关")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
TergentEmptyState(
|
||||||
|
title = "暂无已配对设备",
|
||||||
|
subtitle = "发现并配对网关以开始使用",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovered gateways section
|
||||||
|
if (discoveredGateways.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
TergentSectionTitle(title = "发现的网关")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
DiscoveredGatewaysPanel(
|
||||||
|
gateways = discoveredGateways,
|
||||||
|
onGatewayClick = onGatewayClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
TergentSectionTitle(title = "发现的网关")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
TergentEmptyState(
|
||||||
|
title = "未发现网关",
|
||||||
|
subtitle = "确保网关设备在同一网络中",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add manual gateway button
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
AddGatewayButton(onClick = onAddManualGateway)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Paired devices panel
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun PairedDevicesPanel(
|
||||||
|
devices: List<PairedDevice>,
|
||||||
|
onDeviceClick: (PairedDevice) -> Unit,
|
||||||
|
) {
|
||||||
|
TergentCard(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||||
|
devices.forEachIndexed { index, device ->
|
||||||
|
PairedDeviceRow(device = device, onClick = { onDeviceClick(device) })
|
||||||
|
if (index != devices.lastIndex) {
|
||||||
|
HorizontalDivider(color = border, thickness = 1.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PairedDeviceRow(
|
||||||
|
device: PairedDevice,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.heightIn(min = 52.dp)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||||
|
) {
|
||||||
|
// Green dot
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(success),
|
||||||
|
)
|
||||||
|
// Device icon
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Devices,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = text2,
|
||||||
|
)
|
||||||
|
// Name + status
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||||
|
Text(
|
||||||
|
text = device.name,
|
||||||
|
style = ClawTheme.type.body,
|
||||||
|
color = text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = device.status,
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text2,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Port
|
||||||
|
TergentPortPill(port = device.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TergentPortPill(port: String) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
color = surface,
|
||||||
|
border = BorderStroke(1.dp, border),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = port,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text3,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Discovered gateways panel
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun DiscoveredGatewaysPanel(
|
||||||
|
gateways: List<DiscoveredGateway>,
|
||||||
|
onGatewayClick: (DiscoveredGateway) -> Unit,
|
||||||
|
) {
|
||||||
|
TergentCard(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||||
|
gateways.forEachIndexed { index, gateway ->
|
||||||
|
DiscoveredGatewayRow(gateway = gateway, onClick = { onGatewayClick(gateway) })
|
||||||
|
if (index != gateways.lastIndex) {
|
||||||
|
HorizontalDivider(color = border, thickness = 1.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DiscoveredGatewayRow(
|
||||||
|
gateway: DiscoveredGateway,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.heightIn(min = 52.dp)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||||
|
) {
|
||||||
|
// Gray dot
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(text3),
|
||||||
|
)
|
||||||
|
// Wifi icon
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Wifi,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = text2,
|
||||||
|
)
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
text = gateway.name,
|
||||||
|
style = ClawTheme.type.body,
|
||||||
|
color = text,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
// Tap to pair hint
|
||||||
|
Text(
|
||||||
|
text = "点击配对",
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = primary,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Add gateway button
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun AddGatewayButton(onClick: () -> Unit) {
|
||||||
|
Surface(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = Color.Transparent,
|
||||||
|
border = BorderStroke(1.dp, border.copy(alpha = 0.6f)),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = primary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = "手动添加网关",
|
||||||
|
style = ClawTheme.type.label,
|
||||||
|
color = primary,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared composables (also used by NotificationsScreen & DiagnosticsScreen)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
internal fun TergentScaffold(content: @Composable () -> Unit) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(bg),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TergentTopBar(
|
||||||
|
title: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 4.dp, top = 4.dp, end = 16.dp, bottom = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
onClick = onBack,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = Color.Transparent,
|
||||||
|
contentColor = text,
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = ClawTheme.type.title,
|
||||||
|
color = text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TergentSectionTitle(title: String) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text3,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TergentCard(
|
||||||
|
contentPadding: PaddingValues = PaddingValues(9.dp),
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = surface,
|
||||||
|
border = BorderStroke(1.dp, border),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(contentPadding)) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TergentEmptyState(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
) {
|
||||||
|
TergentCard {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Text(text = title, style = ClawTheme.type.section, color = text)
|
||||||
|
Text(text = subtitle, style = ClawTheme.type.body, color = text2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TergentToggleRow(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp).padding(horizontal = 12.dp, vertical = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = ClawTheme.type.body,
|
||||||
|
color = text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text2,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors =
|
||||||
|
SwitchDefaults.colors(
|
||||||
|
checkedTrackColor = primary,
|
||||||
|
uncheckedTrackColor = surface,
|
||||||
|
uncheckedBorderColor = border,
|
||||||
|
checkedThumbColor = Color.White,
|
||||||
|
uncheckedThumbColor = text2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Placeholder remember functions (to be replaced with actual data binding)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun rememberPairedDevices(): List<PairedDevice> =
|
||||||
|
remember {
|
||||||
|
listOf(
|
||||||
|
PairedDevice(name = "OpenClaw Gateway", status = "已连接 · 局域网", port = "443"),
|
||||||
|
PairedDevice(name = "MacBook Pro", status = "在线 · IPFS", port = "9011"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberDiscoveredGateways(): List<DiscoveredGateway> =
|
||||||
|
remember {
|
||||||
|
listOf(
|
||||||
|
DiscoveredGateway(name = "OpenClaw Gateway-2.4G"),
|
||||||
|
DiscoveredGateway(name = "OpenClaw Gateway-Z-Wave"),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
package ai.openclaw.app.ui.tergent
|
||||||
|
|
||||||
|
import ai.openclaw.app.ui.design.ClawTheme
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Cloud
|
||||||
|
import androidx.compose.material.icons.filled.Lan
|
||||||
|
import androidx.compose.material.icons.filled.Repeat
|
||||||
|
import androidx.compose.material.icons.filled.SignalCellularAlt
|
||||||
|
import androidx.compose.material.icons.filled.SwapHoriz
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Colors (same palette)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
private val bg = Color(0xFF111827)
|
||||||
|
private val surface = Color(0xFF1F2937)
|
||||||
|
private val border = Color(0xFF374151)
|
||||||
|
private val text = Color(0xFFF9FAFB)
|
||||||
|
private val text2 = Color(0xFF9CA3AF)
|
||||||
|
private val text3 = Color(0xFF6B7280)
|
||||||
|
private val primary = Color(0xFF5B8CFF)
|
||||||
|
private val success = Color(0xFF34D399)
|
||||||
|
private val danger = Color(0xFFF87171)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DiagnosticsScreen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
internal fun DiagnosticsScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
TergentScaffold {
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp),
|
||||||
|
) {
|
||||||
|
// Top bar
|
||||||
|
item {
|
||||||
|
TergentTopBar(title = "诊断", onBack = onBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status card
|
||||||
|
item {
|
||||||
|
StatusCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latency history placeholder
|
||||||
|
item {
|
||||||
|
LatencyHistoryCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network info card
|
||||||
|
item {
|
||||||
|
NetworkInfoCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Status card
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun StatusCard() {
|
||||||
|
TergentCard {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "连接状态",
|
||||||
|
style = ClawTheme.type.section,
|
||||||
|
color = text,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
StatusMetricCard(
|
||||||
|
icon = Icons.Default.SignalCellularAlt,
|
||||||
|
label = "状态",
|
||||||
|
value = "已连接",
|
||||||
|
valueColor = success,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
StatusMetricCard(
|
||||||
|
icon = Icons.Default.SwapHoriz,
|
||||||
|
label = "延迟",
|
||||||
|
value = "12 ms",
|
||||||
|
valueColor = text,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
StatusMetricCard(
|
||||||
|
icon = Icons.Default.Cloud,
|
||||||
|
label = "传输",
|
||||||
|
value = "2.3 MB",
|
||||||
|
valueColor = text,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
StatusMetricCard(
|
||||||
|
icon = Icons.Default.Repeat,
|
||||||
|
label = "重连次数",
|
||||||
|
value = "3",
|
||||||
|
valueColor = text,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusMetricCard(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
valueColor: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = Color(0xFF111827),
|
||||||
|
border = BorderStroke(1.dp, border),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = text2,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text3,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = ClawTheme.type.label,
|
||||||
|
color = valueColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Latency history card
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun LatencyHistoryCard() {
|
||||||
|
TergentCard {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "延迟历史",
|
||||||
|
style = ClawTheme.type.section,
|
||||||
|
color = text,
|
||||||
|
)
|
||||||
|
// Placeholder area for latency chart
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
color = Color(0xFF111827),
|
||||||
|
border = BorderStroke(1.dp, border),
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = "延迟趋势",
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text3,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "图表占位区域",
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text3.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Network info card
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun NetworkInfoCard() {
|
||||||
|
TergentCard {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "网络信息",
|
||||||
|
style = ClawTheme.type.section,
|
||||||
|
color = text,
|
||||||
|
)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = Color(0xFF111827),
|
||||||
|
border = BorderStroke(1.dp, border),
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||||
|
NetworkInfoRow(label = "网关", value = "192.168.1.1")
|
||||||
|
HorizontalDividerWithBorder()
|
||||||
|
NetworkInfoRow(label = "本地 IP", value = "192.168.1.35")
|
||||||
|
HorizontalDividerWithBorder()
|
||||||
|
NetworkInfoRow(label = "DNS", value = "8.8.8.8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NetworkInfoRow(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = ClawTheme.type.body,
|
||||||
|
color = text2,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = ClawTheme.type.body,
|
||||||
|
color = text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HorizontalDividerWithBorder() {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.height(1.dp)
|
||||||
|
.background(border),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package ai.openclaw.app.ui.tergent
|
||||||
|
|
||||||
|
import ai.openclaw.app.ui.design.ClawTheme
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Colors (same palette as DevicesScreen)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
private val bg = Color(0xFF111827)
|
||||||
|
private val surface = Color(0xFF1F2937)
|
||||||
|
private val border = Color(0xFF374151)
|
||||||
|
private val text = Color(0xFFF9FAFB)
|
||||||
|
private val text2 = Color(0xFF9CA3AF)
|
||||||
|
private val text3 = Color(0xFF6B7280)
|
||||||
|
private val primary = Color(0xFF5B8CFF)
|
||||||
|
private val danger = Color(0xFFF87171)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Data models
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
internal data class NotificationEntry(
|
||||||
|
val id: String,
|
||||||
|
val source: String,
|
||||||
|
val title: String,
|
||||||
|
val time: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// NotificationsScreen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
internal fun NotificationsScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
notifications: List<NotificationEntry> = rememberNotifications(),
|
||||||
|
onNotificationClick: (NotificationEntry) -> Unit = {},
|
||||||
|
) {
|
||||||
|
var forwardEnabled by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
TergentScaffold {
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp),
|
||||||
|
) {
|
||||||
|
// Top bar
|
||||||
|
item {
|
||||||
|
TergentTopBar(title = "通知", onBack = onBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward toggle
|
||||||
|
item {
|
||||||
|
TergentCard(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||||
|
TergentToggleRow(
|
||||||
|
title = "通知转发",
|
||||||
|
subtitle = "将设备通知转发到此手机",
|
||||||
|
checked = forwardEnabled,
|
||||||
|
onCheckedChange = { forwardEnabled = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section title
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
TergentSectionTitle(title = "通知历史")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification list
|
||||||
|
if (notifications.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
TergentCard(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||||
|
notifications.forEachIndexed { index, notification ->
|
||||||
|
NotificationRow(
|
||||||
|
notification = notification,
|
||||||
|
onClick = { onNotificationClick(notification) },
|
||||||
|
)
|
||||||
|
if (index != notifications.lastIndex) {
|
||||||
|
HorizontalDivider(color = border, thickness = 1.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
TergentEmptyState(
|
||||||
|
title = "暂无通知",
|
||||||
|
subtitle = "当有通知转发到此设备时,将在此显示",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Notification row
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun NotificationRow(
|
||||||
|
notification: NotificationEntry,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 52.dp)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||||
|
) {
|
||||||
|
// Bell icon
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp).padding(top = 2.dp),
|
||||||
|
tint = text2,
|
||||||
|
)
|
||||||
|
// Content
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = notification.title,
|
||||||
|
style = ClawTheme.type.body,
|
||||||
|
color = text,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = notification.source,
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text3,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Time
|
||||||
|
Text(
|
||||||
|
text = notification.time,
|
||||||
|
style = ClawTheme.type.caption,
|
||||||
|
color = text3,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Placeholder data
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
@Composable
|
||||||
|
private fun rememberNotifications(): List<NotificationEntry> =
|
||||||
|
remember {
|
||||||
|
listOf(
|
||||||
|
NotificationEntry(
|
||||||
|
id = "1",
|
||||||
|
source = "OpenClaw Gateway",
|
||||||
|
title = "新设备请求配对:iPhone 15 Pro",
|
||||||
|
time = "5分钟前",
|
||||||
|
),
|
||||||
|
NotificationEntry(
|
||||||
|
id = "2",
|
||||||
|
source = "OpenClaw Gateway",
|
||||||
|
title = "节点离线:MacBook Pro",
|
||||||
|
time = "23分钟前",
|
||||||
|
),
|
||||||
|
NotificationEntry(
|
||||||
|
id = "3",
|
||||||
|
source = "HomeAssistant",
|
||||||
|
title = "客厅温度超过 30°C,已触发空调自动调节",
|
||||||
|
time = "1小时前",
|
||||||
|
),
|
||||||
|
NotificationEntry(
|
||||||
|
id = "4",
|
||||||
|
source = "OpenClaw Gateway",
|
||||||
|
title = "固件更新可用:v2.1.0",
|
||||||
|
time = "3小时前",
|
||||||
|
),
|
||||||
|
NotificationEntry(
|
||||||
|
id = "5",
|
||||||
|
source = "OpenClaw Gateway",
|
||||||
|
title = "自动备份完成 · 2026-05-23",
|
||||||
|
time = "5小时前",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user