fix(scene): address bot review on fake executor delays, time input validation, and localization

This commit is contained in:
2026-08-25 17:35:32 +08:00
parent 56451be0d8
commit b48079564f
7 changed files with 329 additions and 100 deletions
@@ -223,6 +223,14 @@ fun SceneEditorScreen(
}
}
if (state.triggers.isEmpty()) {
Text(
text = stringResource(R.string.scene_empty_triggers_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
state.triggers.forEach { trigger ->
TriggerItemRow(trigger = trigger, onRemove = { viewModel.removeTrigger(trigger.id) })
}
@@ -273,6 +281,14 @@ fun SceneEditorScreen(
}
}
if (state.conditions.isEmpty()) {
Text(
text = stringResource(R.string.scene_empty_conditions_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
state.conditions.forEach { condition ->
ConditionItemRow(condition = condition, onRemove = { viewModel.removeCondition(condition.id) })
}
@@ -327,7 +343,7 @@ fun SceneEditorScreen(
if (state.actions.isEmpty()) {
Text(
text = "暂未添加动作,请点击下方按钮添加",
text = stringResource(R.string.scene_empty_actions_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -514,14 +530,17 @@ fun SceneEditorScreen(
}
}
val defaultSensorName = stringResource(R.string.device_temperature_humidity_title)
val defaultSocketName = stringResource(R.string.scene_default_socket_name)
if (showTriggerDialog) {
AddTriggerDialog(
onDismiss = { showTriggerDialog = false },
onAdd = { triggerType, time, temp, hum, op ->
when (triggerType) {
0 -> viewModel.addTimeTrigger(time)
1 -> viewModel.addTemperatureTrigger("温湿度传感器", op, temp)
2 -> viewModel.addHumidityTrigger("温湿度传感器", op, hum)
1 -> viewModel.addTemperatureTrigger(defaultSensorName, op, temp)
2 -> viewModel.addHumidityTrigger(defaultSensorName, op, hum)
}
showTriggerDialog = false
},
@@ -532,15 +551,15 @@ fun SceneEditorScreen(
AddConditionDialog(
onDismiss = { showConditionDialog = false },
onAddTemp = { op, temp ->
viewModel.addTemperatureCondition("温湿度传感器", op, temp)
viewModel.addTemperatureCondition(defaultSensorName, op, temp)
showConditionDialog = false
},
onAddHumidity = { op, hum ->
viewModel.addHumidityCondition("温湿度传感器", op, hum)
viewModel.addHumidityCondition(defaultSensorName, op, hum)
showConditionDialog = false
},
onAddSocket = { powerState ->
viewModel.addSocketPowerCondition("Zigbee插座控制器", powerState)
viewModel.addSocketPowerCondition(defaultSocketName, powerState)
showConditionDialog = false
},
onAddTimeRange = { start, end ->
@@ -588,10 +607,10 @@ private fun TriggerItemRow(trigger: SceneTrigger, onRemove: () -> Unit) {
horizontalArrangement = Arrangement.SpaceBetween,
) {
val (icon, desc) = when (trigger) {
is SceneTrigger.Time -> Pair("⏰", "定时: ${trigger.timeOfDay}")
is SceneTrigger.Temperature -> Pair("🌡️", "温度 ${trigger.operator.symbol} ${trigger.thresholdCelsius}°C")
is SceneTrigger.Humidity -> Pair("💧", "湿度 ${trigger.operator.symbol} ${trigger.thresholdPercent}%")
is SceneTrigger.DeviceState -> Pair("🔌", "${trigger.deviceName.value} 状态变为 ${trigger.expectedState}")
is SceneTrigger.Time -> Pair("⏰", stringResource(R.string.scene_trigger_type_time) + ": ${trigger.timeOfDay}")
is SceneTrigger.Temperature -> Pair("🌡️", stringResource(R.string.scene_trigger_desc_temp, trigger.operator.symbol, trigger.thresholdCelsius.toInt()))
is SceneTrigger.Humidity -> Pair("💧", stringResource(R.string.scene_trigger_desc_humidity, trigger.operator.symbol, trigger.thresholdPercent.toInt()))
is SceneTrigger.DeviceState -> Pair("🔌", stringResource(R.string.scene_trigger_desc_device, trigger.deviceName.value, trigger.expectedState))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(icon, style = MaterialTheme.typography.titleMedium)
@@ -618,10 +637,10 @@ private fun ConditionItemRow(condition: SceneCondition, onRemove: () -> Unit) {
horizontalArrangement = Arrangement.SpaceBetween,
) {
val (icon, desc) = when (condition) {
is SceneCondition.Temperature -> Pair("🌡️", "温度需 ${condition.operator.symbol} ${condition.thresholdCelsius}°C")
is SceneCondition.Humidity -> Pair("💧", "湿度需 ${condition.operator.symbol} ${condition.thresholdPercent}%")
is SceneCondition.SocketPower -> Pair("🔌", "${condition.deviceName.value} 处于 ${condition.expectedState.name}")
is SceneCondition.TimeRange -> Pair("🕒", "时间在 ${condition.startTime} ~ ${condition.endTime} 之间")
is SceneCondition.Temperature -> Pair("🌡️", stringResource(R.string.scene_condition_desc_temp, condition.operator.symbol, condition.thresholdCelsius.toInt()))
is SceneCondition.Humidity -> Pair("💧", stringResource(R.string.scene_condition_desc_humidity, condition.operator.symbol, condition.thresholdPercent.toInt()))
is SceneCondition.SocketPower -> Pair("🔌", stringResource(R.string.scene_condition_desc_socket, condition.deviceName.value, condition.expectedState.name))
is SceneCondition.TimeRange -> Pair("🕒", stringResource(R.string.scene_condition_desc_time, condition.startTime, condition.endTime))
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(icon, style = MaterialTheme.typography.titleMedium)
@@ -748,15 +767,18 @@ private fun AddTriggerDialog(
var humThreshold by remember { mutableDoubleStateOf(40.0) }
var operator by remember { mutableStateOf(ComparisonOperator.GREATER_THAN) }
val timeRegex = remember { Regex("^([01]\\d|2[0-3]):[0-5]\\d$") }
val isTimeValid = timeRegex.matches(timeInput.trim())
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.scene_dialog_add_trigger_title)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text("⏰ 定时") })
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text("🌡️ 温度") })
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text("💧 湿度") })
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text(stringResource(R.string.scene_chip_time)) })
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text(stringResource(R.string.scene_chip_temp)) })
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text(stringResource(R.string.scene_chip_humidity)) })
}
when (selectedType) {
0 -> {
@@ -765,19 +787,23 @@ private fun AddTriggerDialog(
onValueChange = { timeInput = it },
label = { Text(stringResource(R.string.scene_trigger_time_label)) },
singleLine = true,
isError = !isTimeValid,
supportingText = if (!isTimeValid) {
{ Text(stringResource(R.string.scene_trigger_time_error)) }
} else null,
modifier = Modifier.fillMaxWidth(),
)
}
1, 2 -> {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text("> 高于") })
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text("< 低于") })
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text(stringResource(R.string.scene_chip_above)) })
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text(stringResource(R.string.scene_chip_below)) })
}
if (selectedType == 1) {
Text("设定阈值: ${tempThreshold.toInt()} °C")
Text(stringResource(R.string.scene_threshold_temp, tempThreshold.toInt()))
Slider(value = tempThreshold.toFloat(), onValueChange = { tempThreshold = it.toDouble() }, valueRange = 10f..40f)
} else {
Text("设定阈值: ${humThreshold.toInt()} %")
Text(stringResource(R.string.scene_threshold_hum, humThreshold.toInt()))
Slider(value = humThreshold.toFloat(), onValueChange = { humThreshold = it.toDouble() }, valueRange = 10f..90f)
}
}
@@ -785,7 +811,11 @@ private fun AddTriggerDialog(
}
},
confirmButton = {
Button(onClick = { onAdd(selectedType, timeInput, tempThreshold, humThreshold, operator) }) {
val canConfirm = selectedType != 0 || isTimeValid
Button(
onClick = { onAdd(selectedType, timeInput.trim(), tempThreshold, humThreshold, operator) },
enabled = canConfirm,
) {
Text(stringResource(R.string.scene_add_action))
}
},
@@ -811,57 +841,86 @@ private fun AddConditionDialog(
var startTime by remember { mutableStateOf("22:00") }
var endTime by remember { mutableStateOf("07:00") }
val timeRegex = remember { Regex("^([01]\\d|2[0-3]):[0-5]\\d$") }
val isStartTimeValid = timeRegex.matches(startTime.trim())
val isEndTimeValid = timeRegex.matches(endTime.trim())
val isTimeRangeValid = isStartTimeValid && isEndTimeValid
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.scene_dialog_add_condition_title)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text("🌡️ 温度") })
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text("💧 湿度") })
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text("🔌 插座") })
FilterChip(selected = selectedType == 3, onClick = { selectedType = 3 }, label = { Text("🕒 时间") })
FilterChip(selected = selectedType == 0, onClick = { selectedType = 0 }, label = { Text(stringResource(R.string.scene_chip_temp)) })
FilterChip(selected = selectedType == 1, onClick = { selectedType = 1 }, label = { Text(stringResource(R.string.scene_chip_humidity)) })
FilterChip(selected = selectedType == 2, onClick = { selectedType = 2 }, label = { Text(stringResource(R.string.scene_chip_socket)) })
FilterChip(selected = selectedType == 3, onClick = { selectedType = 3 }, label = { Text(stringResource(R.string.scene_chip_time)) })
}
when (selectedType) {
0 -> {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text("> 高于") })
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text("< 低于") })
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text(stringResource(R.string.scene_chip_above)) })
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text(stringResource(R.string.scene_chip_below)) })
}
Text("设定温度: ${tempVal.toInt()} °C")
Text(stringResource(R.string.scene_threshold_temp, tempVal.toInt()))
Slider(value = tempVal.toFloat(), onValueChange = { tempVal = it.toDouble() }, valueRange = 10f..40f)
}
1 -> {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text("> 高于") })
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text("< 低于") })
FilterChip(selected = operator == ComparisonOperator.GREATER_THAN, onClick = { operator = ComparisonOperator.GREATER_THAN }, label = { Text(stringResource(R.string.scene_chip_above)) })
FilterChip(selected = operator == ComparisonOperator.LESS_THAN, onClick = { operator = ComparisonOperator.LESS_THAN }, label = { Text(stringResource(R.string.scene_chip_below)) })
}
Text("设定湿度: ${humVal.toInt()} %")
Text(stringResource(R.string.scene_threshold_hum, humVal.toInt()))
Slider(value = humVal.toFloat(), onValueChange = { humVal = it.toDouble() }, valueRange = 10f..90f)
}
2 -> {
Text("要求插座当前状态为:")
Text(stringResource(R.string.scene_target_socket_state))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text("开启") })
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text("关闭") })
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text(stringResource(R.string.scene_chip_power_on)) })
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text(stringResource(R.string.scene_chip_power_off)) })
}
}
3 -> {
OutlinedTextField(value = startTime, onValueChange = { startTime = it }, label = { Text(stringResource(R.string.scene_condition_start_time)) }, singleLine = true)
OutlinedTextField(value = endTime, onValueChange = { endTime = it }, label = { Text(stringResource(R.string.scene_condition_end_time)) }, singleLine = true)
OutlinedTextField(
value = startTime,
onValueChange = { startTime = it },
label = { Text(stringResource(R.string.scene_condition_start_time)) },
singleLine = true,
isError = !isStartTimeValid,
supportingText = if (!isStartTimeValid) {
{ Text(stringResource(R.string.scene_condition_time_error)) }
} else null,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = endTime,
onValueChange = { endTime = it },
label = { Text(stringResource(R.string.scene_condition_end_time)) },
singleLine = true,
isError = !isEndTimeValid,
supportingText = if (!isEndTimeValid) {
{ Text(stringResource(R.string.scene_condition_time_error)) }
} else null,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
},
confirmButton = {
Button(onClick = {
when (selectedType) {
0 -> onAddTemp(operator, tempVal)
1 -> onAddHumidity(operator, humVal)
2 -> onAddSocket(socketPower)
3 -> onAddTimeRange(startTime, endTime)
}
}) {
val canConfirm = selectedType != 3 || isTimeRangeValid
Button(
onClick = {
when (selectedType) {
0 -> onAddTemp(operator, tempVal)
1 -> onAddHumidity(operator, humVal)
2 -> onAddSocket(socketPower)
3 -> onAddTimeRange(startTime.trim(), endTime.trim())
}
},
enabled = canConfirm,
) {
Text(stringResource(R.string.scene_add_action))
}
},
@@ -882,17 +941,23 @@ private fun AddActionDialog(
onAddNotify: (title: String, msg: String) -> Unit,
) {
var actionType by remember { mutableIntStateOf(0) }
var socketName by remember { mutableStateOf("Zigbee插座控制器") }
val defaultSocketName = stringResource(R.string.scene_default_socket_name)
val defaultIrName = stringResource(R.string.scene_default_ir_name)
val defaultPowerBtn = stringResource(R.string.scene_default_button_power)
val defaultNotifyTitle = stringResource(R.string.scene_default_notify_title)
val defaultNotifyMsg = stringResource(R.string.scene_default_notify_msg)
var socketName by remember { mutableStateOf(defaultSocketName) }
var socketPower by remember { mutableStateOf(SocketPowerState.ON) }
var selectedRemoteIndex by remember { mutableIntStateOf(0) }
var keyBindings by remember { mutableStateOf<List<RemoteKeyBinding>>(emptyList()) }
var selectedButtonId by remember { mutableStateOf("power") }
var selectedButtonName by remember { mutableStateOf("电源") }
var selectedButtonName by remember { mutableStateOf(defaultPowerBtn) }
var delaySeconds by remember { mutableIntStateOf(3) }
var notifyTitle by remember { mutableStateOf("情景完成") }
var notifyMsg by remember { mutableStateOf("自动化任务执行完成") }
var notifyTitle by remember { mutableStateOf(defaultNotifyTitle) }
var notifyMsg by remember { mutableStateOf(defaultNotifyMsg) }
LaunchedEffect(availableRemotes, selectedRemoteIndex) {
if (availableRemotes.isNotEmpty() && selectedRemoteIndex in availableRemotes.indices) {
@@ -905,7 +970,7 @@ private fun AddActionDialog(
selectedButtonName = bindings[0].displayName.ifBlank { bindings[0].buttonId }
} else {
selectedButtonId = "power"
selectedButtonName = "电源"
selectedButtonName = defaultPowerBtn
}
} else {
keyBindings = emptyList()
@@ -918,10 +983,10 @@ private fun AddActionDialog(
text = {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(selected = actionType == 0, onClick = { actionType = 0 }, label = { Text("🔌 插座") })
FilterChip(selected = actionType == 1, onClick = { actionType = 1 }, label = { Text("📺 遥控") })
FilterChip(selected = actionType == 2, onClick = { actionType = 2 }, label = { Text("⏱️ 延时") })
FilterChip(selected = actionType == 3, onClick = { actionType = 3 }, label = { Text("🔔 提示") })
FilterChip(selected = actionType == 0, onClick = { actionType = 0 }, label = { Text(stringResource(R.string.scene_chip_socket)) })
FilterChip(selected = actionType == 1, onClick = { actionType = 1 }, label = { Text(stringResource(R.string.scene_chip_remote)) })
FilterChip(selected = actionType == 2, onClick = { actionType = 2 }, label = { Text(stringResource(R.string.scene_chip_delay)) })
FilterChip(selected = actionType == 3, onClick = { actionType = 3 }, label = { Text(stringResource(R.string.scene_chip_notify)) })
}
when (actionType) {
0 -> {
@@ -934,14 +999,14 @@ private fun AddActionDialog(
)
Text(stringResource(R.string.scene_socket_power_state))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text("开启") })
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text("关闭") })
FilterChip(selected = socketPower == SocketPowerState.ON, onClick = { socketPower = SocketPowerState.ON }, label = { Text(stringResource(R.string.scene_chip_power_on)) })
FilterChip(selected = socketPower == SocketPowerState.OFF, onClick = { socketPower = SocketPowerState.OFF }, label = { Text(stringResource(R.string.scene_chip_power_off)) })
}
}
1 -> {
if (availableRemotes.isEmpty()) {
Text("未找到虚拟遥控器,将默认使用红外控制器")
OutlinedTextField(value = selectedButtonId, onValueChange = { selectedButtonId = it }, label = { Text("按键名称/槽位") }, singleLine = true)
Text(stringResource(R.string.scene_no_virtual_remotes))
OutlinedTextField(value = selectedButtonId, onValueChange = { selectedButtonId = it }, label = { Text(stringResource(R.string.scene_remote_button_slot_label)) }, singleLine = true)
} else {
Text(stringResource(R.string.scene_select_virtual_remote))
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
@@ -969,12 +1034,12 @@ private fun AddActionDialog(
}
}
} else {
OutlinedTextField(value = selectedButtonName, onValueChange = { selectedButtonName = it; selectedButtonId = it }, label = { Text("按键名称") }, singleLine = true)
OutlinedTextField(value = selectedButtonName, onValueChange = { selectedButtonName = it; selectedButtonId = it }, label = { Text(stringResource(R.string.scene_remote_button_name_label)) }, singleLine = true)
}
}
}
2 -> {
Text(stringResource(R.string.scene_delay_duration_label) + ": $delaySeconds 秒")
Text(stringResource(R.string.scene_delay_duration_label) + ": " + stringResource(R.string.scene_unit_seconds, delaySeconds))
Slider(
value = delaySeconds.toFloat(),
onValueChange = { delaySeconds = it.toInt() },
@@ -998,7 +1063,7 @@ private fun AddActionDialog(
if (remote != null) {
onAddRemote(remote.id.value, remote.displayName, selectedButtonId, selectedButtonName)
} else {
onAddRemote("default-ir", "红外控制器", selectedButtonId, selectedButtonName)
onAddRemote("default-ir", defaultIrName, selectedButtonId, selectedButtonName)
}
}
2 -> onAddDelay(delaySeconds)
@@ -116,7 +116,7 @@ private fun CenteredMessage(paddingValues: PaddingValues, message: String, onRet
Button(onClick = onRetry) {
Icon(Icons.Filled.Refresh, contentDescription = null)
Spacer(Modifier.width(6.dp))
Text("重试")
Text(stringResource(R.string.action_retry))
}
}
}
@@ -137,7 +137,7 @@ private fun ReadyContent(
) {
Text(sceneName, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Text("点击下方按钮立即开始执行", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(stringResource(R.string.scene_exec_ready_hint), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.height(24.dp))
Button(
onClick = onExecute,
@@ -247,14 +247,14 @@ private fun CompletedContent(
) {
Icon(Icons.Filled.Refresh, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(6.dp))
Text("重新执行")
Text(stringResource(R.string.scene_exec_rerun))
}
Button(
onClick = onBack,
modifier = Modifier.weight(1f).height(48.dp),
shape = RoundedCornerShape(12.dp),
) {
Text("返回", fontWeight = FontWeight.SemiBold)
Text(stringResource(R.string.action_back), fontWeight = FontWeight.SemiBold)
}
}
}
@@ -310,7 +310,7 @@ private fun ActionResultRow(stepNumber: Int, entry: SceneActionExecutionResult)
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "步骤 $stepNumber: ${entry.action.describe()}",
text = stringResource(R.string.scene_step_format, stepNumber, entry.action.describe()),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
)
@@ -334,11 +334,12 @@ private fun ActionResultRow(stepNumber: Int, entry: SceneActionExecutionResult)
}
}
@Composable
private fun SceneAction.describe(): String = when (this) {
is SceneAction.SocketPower -> "插座【${deviceName.value}】-> ${powerState.name}"
is SceneAction.VirtualRemoteButton -> "遥控【$remoteDisplayName】-> $buttonDisplayName"
is SceneAction.InfraredKey -> "红外【${deviceName.value}】-> 槽位 $key"
is SceneAction.Delay -> "⏱️ 等待 ${durationSeconds} 秒"
is SceneAction.Notification -> "🔔 提示: $title"
is SceneAction.SocketPower -> stringResource(R.string.scene_action_socket, deviceName.value, powerState.name)
is SceneAction.VirtualRemoteButton -> stringResource(R.string.scene_action_remote, remoteDisplayName, buttonDisplayName)
is SceneAction.InfraredKey -> stringResource(R.string.scene_action_infrared, deviceName.value, key)
is SceneAction.Delay -> stringResource(R.string.scene_action_delay, durationSeconds)
is SceneAction.Notification -> stringResource(R.string.scene_action_notification, title)
}
@@ -305,13 +305,17 @@ private fun SceneCard(
) {
if (scene.type == AutomationType.AUTOMATIC) {
Text(
text = if (scene.triggers.isNotEmpty()) "⚡ ${scene.triggers.size} 个触发器 • ${scene.actions.size} 个动作" else "${scene.actions.size} 个动作",
text = if (scene.triggers.isNotEmpty()) {
stringResource(R.string.scene_summary_triggers_and_actions, scene.triggers.size, scene.actions.size)
} else {
stringResource(R.string.scene_summary_actions_only, scene.actions.size)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
Text(
text = "${scene.actions.size} 个执行动作",
text = stringResource(R.string.scene_summary_actions_only, scene.actions.size),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -161,6 +161,47 @@
<string name="scene_step_failed">执行失败</string>
<string name="scene_step_skipped">已跳过</string>
<!-- 校验与弹窗选项/占位文案 -->
<string name="scene_trigger_time_error">时间格式无效(请按 HH:mm 格式输入,如 07:30)</string>
<string name="scene_condition_time_error">时间格式无效(请按 HH:mm 格式输入,如 22:00)</string>
<string name="scene_chip_time">⏰ 定时</string>
<string name="scene_chip_temp">🌡️ 温度</string>
<string name="scene_chip_humidity">💧 湿度</string>
<string name="scene_chip_socket">🔌 插座</string>
<string name="scene_chip_remote">📺 遥控</string>
<string name="scene_chip_delay">⏱️ 延时</string>
<string name="scene_chip_notify">🔔 提示</string>
<string name="scene_chip_above">&gt; 高于</string>
<string name="scene_chip_below">&lt; 低于</string>
<string name="scene_chip_power_on">开启</string>
<string name="scene_chip_power_off">关闭</string>
<string name="scene_threshold_temp">设定阈值:%1$d °C</string>
<string name="scene_threshold_hum">设定阈值:%1$d %%</string>
<string name="scene_target_socket_state">要求插座当前状态为:</string>
<string name="scene_no_virtual_remotes">未找到虚拟遥控器,将默认使用红外控制器</string>
<string name="scene_remote_button_slot_label">按键名称/槽位</string>
<string name="scene_remote_button_name_label">按键名称</string>
<string name="scene_empty_triggers_hint">尚未添加触发器,请点击下方按钮添加</string>
<string name="scene_empty_conditions_hint">尚未添加限制条件,请点击下方按钮添加</string>
<string name="scene_empty_actions_hint">尚未添加执行动作,请点击下方按钮添加</string>
<string name="scene_summary_triggers_and_actions">⚡ %1$d 个触发器 • %2$d 个动作</string>
<string name="scene_summary_actions_only">%1$d 个执行动作</string>
<string name="scene_exec_ready_hint">点击下方按钮立即开始执行</string>
<string name="scene_exec_rerun">重新执行</string>
<string name="scene_step_format">步骤 %1$d:%2$s</string>
<string name="scene_trigger_desc_temp">温度 %1$s %2$s°C</string>
<string name="scene_trigger_desc_humidity">湿度 %1$s %2$s%%</string>
<string name="scene_trigger_desc_device">%1$s 状态变为 %2$s</string>
<string name="scene_condition_desc_temp">温度需 %1$s %2$s°C</string>
<string name="scene_condition_desc_humidity">湿度需 %1$s %2$s%%</string>
<string name="scene_condition_desc_socket">%1$s 处于 %2$s</string>
<string name="scene_condition_desc_time">时间在 %1$s ~ %2$s 之间</string>
<string name="scene_default_socket_name">Zigbee插座控制器</string>
<string name="scene_default_ir_name">红外控制器</string>
<string name="scene_default_button_power">电源</string>
<string name="scene_default_notify_title">情景完成</string>
<string name="scene_default_notify_msg">自动化任务执行完成</string>
<!-- 设备列表 -->
<string name="device_list_title">设备列表</string>
<string name="device_list_description">选择需要控制或查看状态的 ABox 终端设备。</string>
@@ -161,6 +161,47 @@
<string name="scene_step_failed">Failed</string>
<string name="scene_step_skipped">Skipped</string>
<!-- Validation & dialog chips / placeholders -->
<string name="scene_trigger_time_error">Invalid time format (use HH:mm, e.g. 07:30)</string>
<string name="scene_condition_time_error">Invalid time format (use HH:mm, e.g. 22:00)</string>
<string name="scene_chip_time">⏰ Schedule</string>
<string name="scene_chip_temp">🌡️ Temperature</string>
<string name="scene_chip_humidity">💧 Humidity</string>
<string name="scene_chip_socket">🔌 Socket</string>
<string name="scene_chip_remote">📺 Remote</string>
<string name="scene_chip_delay">⏱️ Delay</string>
<string name="scene_chip_notify">🔔 Notify</string>
<string name="scene_chip_above">&gt; Above</string>
<string name="scene_chip_below">&lt; Below</string>
<string name="scene_chip_power_on">Turn ON</string>
<string name="scene_chip_power_off">Turn OFF</string>
<string name="scene_threshold_temp">Target threshold: %1$d °C</string>
<string name="scene_threshold_hum">Target threshold: %1$d %%</string>
<string name="scene_target_socket_state">Require socket state to be:</string>
<string name="scene_no_virtual_remotes">No virtual remotes found; using infrared controller by default.</string>
<string name="scene_remote_button_slot_label">Button Name / Slot</string>
<string name="scene_remote_button_name_label">Button Name</string>
<string name="scene_empty_triggers_hint">No triggers configured. Tap + below to add.</string>
<string name="scene_empty_conditions_hint">No conditions configured. Tap + below to add.</string>
<string name="scene_empty_actions_hint">No actions configured. Tap + below to add.</string>
<string name="scene_summary_triggers_and_actions">⚡ %1$d triggers • %2$d actions</string>
<string name="scene_summary_actions_only">%1$d actions</string>
<string name="scene_exec_ready_hint">Tap the button below to run immediately</string>
<string name="scene_exec_rerun">Re-run</string>
<string name="scene_step_format">Step %1$d: %2$s</string>
<string name="scene_trigger_desc_temp">Temp %1$s %2$s°C</string>
<string name="scene_trigger_desc_humidity">Humidity %1$s %2$s%%</string>
<string name="scene_trigger_desc_device">%1$s state changed to %2$s</string>
<string name="scene_condition_desc_temp">Temp must be %1$s %2$s°C</string>
<string name="scene_condition_desc_humidity">Humidity must be %1$s %2$s%%</string>
<string name="scene_condition_desc_socket">%1$s is %2$s</string>
<string name="scene_condition_desc_time">Time is between %1$s ~ %2$s</string>
<string name="scene_default_socket_name">Zigbee Smart Socket</string>
<string name="scene_default_ir_name">Infrared Controller</string>
<string name="scene_default_button_power">Power</string>
<string name="scene_default_notify_title">Scene Completed</string>
<string name="scene_default_notify_msg">Automation task executed successfully</string>
<!-- Device list -->
<string name="device_list_title">Device List</string>
<string name="device_list_description">Select an ABox terminal device to control or check status.</string>