feat(tergent): mirror DevicesScreen, DiagnosticsScreen, NotificationsScreen to app/src/main/java

This commit is contained in:
茂之钳
2026-05-23 07:26:26 +00:00
parent 2ff4bdf4f4
commit 5f03ffc305
3 changed files with 1019 additions and 0 deletions
@@ -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小时前",
),
)
}