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