feat(ui): complete ABox app experience redesign #10

Merged
KremeCN merged 40 commits from feat/ui-ux-redesign into main 2026-08-27 23:25:08 +08:00
12 changed files with 173 additions and 159 deletions
Showing only changes of commit d8d9eb4f28 - Show all commits
@@ -747,7 +747,7 @@ private fun ActionItemRow(
@Composable @Composable
private fun SceneTrigger.describe(): String = when (this) { private fun SceneTrigger.describe(): String = when (this) {
is SceneTrigger.Time -> "⏰ $timeOfDay" is SceneTrigger.Time -> "定时 $timeOfDay"
is SceneTrigger.Temperature -> stringResource(R.string.scene_trigger_desc_temp, operator.symbol, thresholdCelsius.toInt().toString()) is SceneTrigger.Temperature -> stringResource(R.string.scene_trigger_desc_temp, operator.symbol, thresholdCelsius.toInt().toString())
is SceneTrigger.Humidity -> stringResource(R.string.scene_trigger_desc_humidity, operator.symbol, thresholdPercent.toInt().toString()) is SceneTrigger.Humidity -> stringResource(R.string.scene_trigger_desc_humidity, operator.symbol, thresholdPercent.toInt().toString())
is SceneTrigger.DeviceState -> stringResource(R.string.scene_trigger_desc_device, deviceName.value, expectedState) is SceneTrigger.DeviceState -> stringResource(R.string.scene_trigger_desc_device, deviceName.value, expectedState)
@@ -68,6 +68,8 @@ import com.flagship.abox.manager.domain.Scene
import com.flagship.abox.manager.domain.SceneColor import com.flagship.abox.manager.domain.SceneColor
import com.flagship.abox.manager.domain.SceneIcon import com.flagship.abox.manager.domain.SceneIcon
import com.flagship.abox.manager.domain.SceneId import com.flagship.abox.manager.domain.SceneId
import com.flagship.abox.manager.designsystem.icon.ABoxIcon
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -250,9 +252,10 @@ private fun SceneCard(
), ),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text( ABoxIcon(
text = scene.icon.displayEmoji(), key = scene.icon.toABoxIconKey(),
style = MaterialTheme.typography.titleMedium, contentDescription = scene.name,
modifier = Modifier.size(28.dp),
) )
} }
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
@@ -375,10 +378,7 @@ private fun EmptySceneList(modifier: Modifier = Modifier) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text( ABoxIcon(ABoxIconKey.SCENES, contentDescription = "暂无情景", modifier = Modifier.size(48.dp))
text = "⚡",
style = MaterialTheme.typography.displayMedium,
)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
Text( Text(
text = stringResource(R.string.scene_empty_title), text = stringResource(R.string.scene_empty_title),
@@ -396,19 +396,19 @@ private fun EmptySceneList(modifier: Modifier = Modifier) {
} }
} }
fun SceneIcon.displayEmoji(): String = when (this) { fun SceneIcon.toABoxIconKey(): ABoxIconKey = when (this) {
SceneIcon.HOME -> "🏠" SceneIcon.HOME -> ABoxIconKey.DEVICES
SceneIcon.OFFICE -> "💼" SceneIcon.OFFICE -> ABoxIconKey.ACTIVITY
SceneIcon.NIGHT -> "🌙" SceneIcon.NIGHT -> ABoxIconKey.PENDING
SceneIcon.MOVIE -> "🎬" SceneIcon.MOVIE -> ABoxIconKey.SCENES
SceneIcon.AWAY -> "🚶" SceneIcon.AWAY -> ABoxIconKey.OFFLINE
SceneIcon.READING -> "📖" SceneIcon.READING -> ABoxIconKey.DEVICES
SceneIcon.DINING -> "🍽️" SceneIcon.DINING -> ABoxIconKey.DEVICES
SceneIcon.GAMING -> "🎮" SceneIcon.GAMING -> ABoxIconKey.SCENES
SceneIcon.THERMOSTAT -> "🌡️" SceneIcon.THERMOSTAT -> ABoxIconKey.TEMPERATURE
SceneIcon.LIGHT -> "💡" SceneIcon.LIGHT -> ABoxIconKey.POWER
SceneIcon.CLEAN -> "🧹" SceneIcon.CLEAN -> ABoxIconKey.REFRESH
SceneIcon.CUSTOM -> "✨" SceneIcon.CUSTOM -> ABoxIconKey.UNKNOWN_DEVICE
} }
internal fun SceneColor.toColorOrNull(): Color? { internal fun SceneColor.toColorOrNull(): Color? {
@@ -82,9 +82,9 @@
<string name="scene_section_other">其他选项与外观</string> <string name="scene_section_other">其他选项与外观</string>
<!-- Add Buttons --> <!-- Add Buttons -->
<string name="scene_add_trigger">+ 新增触发器</string> <string name="scene_add_trigger">新增触发器</string>
<string name="scene_add_condition">+ 新增条件</string> <string name="scene_add_condition">新增条件</string>
<string name="scene_add_action_btn">+ 新增动作</string> <string name="scene_add_action_btn">新增动作</string>
<string name="scene_add_action">添加</string> <string name="scene_add_action">添加</string>
<string name="scene_save_action">保存指令</string> <string name="scene_save_action">保存指令</string>
@@ -104,10 +104,10 @@
<!-- Trigger Types & Dialog --> <!-- Trigger Types & Dialog -->
<string name="scene_dialog_add_trigger_title">选择触发器类型</string> <string name="scene_dialog_add_trigger_title">选择触发器类型</string>
<string name="scene_trigger_type_time">⏰ 定时时间触发</string> <string name="scene_trigger_type_time">定时时间触发</string>
<string name="scene_trigger_type_temp">🌡️ 环境温度触发</string> <string name="scene_trigger_type_temp">环境温度触发</string>
<string name="scene_trigger_type_humidity">💧 环境湿度触发</string> <string name="scene_trigger_type_humidity">环境湿度触发</string>
<string name="scene_trigger_type_device">🔌 插座状态变更触发</string> <string name="scene_trigger_type_device">插座状态变更触发</string>
<string name="scene_trigger_time_label">触发时间点(如:07:30)</string> <string name="scene_trigger_time_label">触发时间点(如:07:30)</string>
<string name="scene_trigger_temp_threshold">温度阈值 (°C)</string> <string name="scene_trigger_temp_threshold">温度阈值 (°C)</string>
<string name="scene_trigger_humidity_threshold">湿度阈值 (%)</string> <string name="scene_trigger_humidity_threshold">湿度阈值 (%)</string>
@@ -118,19 +118,19 @@
<!-- Condition Types & Dialog --> <!-- Condition Types & Dialog -->
<string name="scene_dialog_add_condition_title">选择限制条件</string> <string name="scene_dialog_add_condition_title">选择限制条件</string>
<string name="scene_condition_type_temp">🌡️ 环境温度范围</string> <string name="scene_condition_type_temp">环境温度范围</string>
<string name="scene_condition_type_humidity">💧 环境湿度范围</string> <string name="scene_condition_type_humidity">环境湿度范围</string>
<string name="scene_condition_type_socket">🔌 插座当前电源状态</string> <string name="scene_condition_type_socket">插座当前电源状态</string>
<string name="scene_condition_type_time_range">🕒 生效时间区间</string> <string name="scene_condition_type_time_range">生效时间区间</string>
<string name="scene_condition_start_time">起始时间(如:22:00)</string> <string name="scene_condition_start_time">起始时间(如:22:00)</string>
<string name="scene_condition_end_time">结束时间(如:07:00)</string> <string name="scene_condition_end_time">结束时间(如:07:00)</string>
<!-- Action Types & Dialog --> <!-- Action Types & Dialog -->
<string name="scene_dialog_add_action_title">选择执行动作</string> <string name="scene_dialog_add_action_title">选择执行动作</string>
<string name="scene_action_type_socket">🔌 智能插座控制</string> <string name="scene_action_type_socket">智能插座控制</string>
<string name="scene_action_type_virtual_remote">📺 红外遥控按键</string> <string name="scene_action_type_virtual_remote">红外遥控按键</string>
<string name="scene_action_type_delay">⏱️ 延时等待 (缓冲)</string> <string name="scene_action_type_delay">延时等待(缓冲)</string>
<string name="scene_action_type_notification">🔔 发送系统提示</string> <string name="scene_action_type_notification">发送系统提示</string>
<string name="scene_select_socket">选择目标插座</string> <string name="scene_select_socket">选择目标插座</string>
<string name="scene_socket_power_state">设定电源状态</string> <string name="scene_socket_power_state">设定电源状态</string>
<string name="scene_select_virtual_remote">选择虚拟遥控器</string> <string name="scene_select_virtual_remote">选择虚拟遥控器</string>
@@ -144,8 +144,8 @@
<string name="scene_action_socket">插座【%1$s】: 设为 %2$s</string> <string name="scene_action_socket">插座【%1$s】: 设为 %2$s</string>
<string name="scene_action_remote">遥控【%1$s】: 发送 %2$s</string> <string name="scene_action_remote">遥控【%1$s】: 发送 %2$s</string>
<string name="scene_action_infrared">红外【%1$s】: 槽位 %2$s</string> <string name="scene_action_infrared">红外【%1$s】: 槽位 %2$s</string>
<string name="scene_action_delay">⏱️ 等待 %1$d 秒</string> <string name="scene_action_delay">等待 %1$d 秒</string>
<string name="scene_action_notification">🔔 提示: %1$s</string> <string name="scene_action_notification">提示:%1$s</string>
<!-- Scene: execution --> <!-- Scene: execution -->
<string name="scene_execution_title">执行指令</string> <string name="scene_execution_title">执行指令</string>
@@ -164,13 +164,13 @@
<!-- 校验与弹窗选项/占位文案 --> <!-- 校验与弹窗选项/占位文案 -->
<string name="scene_trigger_time_error">时间格式无效(请按 HH:mm 格式输入,如 07:30)</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_condition_time_error">时间格式无效(请按 HH:mm 格式输入,如 22:00)</string>
<string name="scene_chip_time">⏰ 定时</string> <string name="scene_chip_time">定时</string>
<string name="scene_chip_temp">🌡️ 温度</string> <string name="scene_chip_temp">温度</string>
<string name="scene_chip_humidity">💧 湿度</string> <string name="scene_chip_humidity">湿度</string>
<string name="scene_chip_socket">🔌 插座</string> <string name="scene_chip_socket">插座</string>
<string name="scene_chip_remote">📺 遥控</string> <string name="scene_chip_remote">遥控</string>
<string name="scene_chip_delay">⏱️ 延时</string> <string name="scene_chip_delay">延时</string>
<string name="scene_chip_notify">🔔 提示</string> <string name="scene_chip_notify">提示</string>
<string name="scene_chip_above">&gt; 高于</string> <string name="scene_chip_above">&gt; 高于</string>
<string name="scene_chip_below">&lt; 低于</string> <string name="scene_chip_below">&lt; 低于</string>
<string name="scene_chip_power_on">开启</string> <string name="scene_chip_power_on">开启</string>
@@ -184,7 +184,7 @@
<string name="scene_empty_triggers_hint">尚未添加触发器,请点击下方按钮添加</string> <string name="scene_empty_triggers_hint">尚未添加触发器,请点击下方按钮添加</string>
<string name="scene_empty_conditions_hint">尚未添加限制条件,请点击下方按钮添加</string> <string name="scene_empty_conditions_hint">尚未添加限制条件,请点击下方按钮添加</string>
<string name="scene_empty_actions_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_triggers_and_actions">%1$d 个触发器 · %2$d 个动作</string>
<string name="scene_summary_actions_only">%1$d 个执行动作</string> <string name="scene_summary_actions_only">%1$d 个执行动作</string>
<string name="scene_exec_ready_hint">点击下方按钮立即开始执行</string> <string name="scene_exec_ready_hint">点击下方按钮立即开始执行</string>
<string name="scene_exec_rerun">重新执行</string> <string name="scene_exec_rerun">重新执行</string>
+25 -25
View File
@@ -82,9 +82,9 @@
<string name="scene_section_other">Other Options &amp; Appearance</string> <string name="scene_section_other">Other Options &amp; Appearance</string>
<!-- Add Buttons --> <!-- Add Buttons -->
<string name="scene_add_trigger">+ Add Trigger</string> <string name="scene_add_trigger">Add Trigger</string>
<string name="scene_add_condition">+ Add Condition</string> <string name="scene_add_condition">Add Condition</string>
<string name="scene_add_action_btn">+ Add Action</string> <string name="scene_add_action_btn">Add Action</string>
<string name="scene_add_action">Add</string> <string name="scene_add_action">Add</string>
<string name="scene_save_action">Save Command</string> <string name="scene_save_action">Save Command</string>
@@ -104,10 +104,10 @@
<!-- Trigger Types & Dialog --> <!-- Trigger Types & Dialog -->
<string name="scene_dialog_add_trigger_title">Select Trigger Type</string> <string name="scene_dialog_add_trigger_title">Select Trigger Type</string>
<string name="scene_trigger_type_time">⏰ Time / Schedule Trigger</string> <string name="scene_trigger_type_time">Time / Schedule Trigger</string>
<string name="scene_trigger_type_temp">🌡️ Temperature Threshold</string> <string name="scene_trigger_type_temp">Temperature Threshold</string>
<string name="scene_trigger_type_humidity">💧 Humidity Threshold</string> <string name="scene_trigger_type_humidity">Humidity Threshold</string>
<string name="scene_trigger_type_device">🔌 Socket State Changed</string> <string name="scene_trigger_type_device">Socket State Changed</string>
<string name="scene_trigger_time_label">Trigger Time (e.g. 07:30)</string> <string name="scene_trigger_time_label">Trigger Time (e.g. 07:30)</string>
<string name="scene_trigger_temp_threshold">Temperature Threshold (°C)</string> <string name="scene_trigger_temp_threshold">Temperature Threshold (°C)</string>
<string name="scene_trigger_humidity_threshold">Humidity Threshold (%)</string> <string name="scene_trigger_humidity_threshold">Humidity Threshold (%)</string>
@@ -118,19 +118,19 @@
<!-- Condition Types & Dialog --> <!-- Condition Types & Dialog -->
<string name="scene_dialog_add_condition_title">Select Condition</string> <string name="scene_dialog_add_condition_title">Select Condition</string>
<string name="scene_condition_type_temp">🌡️ Temperature Range</string> <string name="scene_condition_type_temp">Temperature Range</string>
<string name="scene_condition_type_humidity">💧 Humidity Range</string> <string name="scene_condition_type_humidity">Humidity Range</string>
<string name="scene_condition_type_socket">🔌 Current Socket Power</string> <string name="scene_condition_type_socket">Current Socket Power</string>
<string name="scene_condition_type_time_range">🕒 Active Time Range</string> <string name="scene_condition_type_time_range">Active Time Range</string>
<string name="scene_condition_start_time">Start Time (e.g. 22:00)</string> <string name="scene_condition_start_time">Start Time (e.g. 22:00)</string>
<string name="scene_condition_end_time">End Time (e.g. 07:00)</string> <string name="scene_condition_end_time">End Time (e.g. 07:00)</string>
<!-- Action Types & Dialog --> <!-- Action Types & Dialog -->
<string name="scene_dialog_add_action_title">Select Action</string> <string name="scene_dialog_add_action_title">Select Action</string>
<string name="scene_action_type_socket">🔌 Smart Socket Control</string> <string name="scene_action_type_socket">Smart Socket Control</string>
<string name="scene_action_type_virtual_remote">📺 Infrared Remote Button</string> <string name="scene_action_type_virtual_remote">Infrared Remote Button</string>
<string name="scene_action_type_delay">⏱️ Delay / Buffer Timer</string> <string name="scene_action_type_delay">Delay / Buffer Timer</string>
<string name="scene_action_type_notification">🔔 System Notification</string> <string name="scene_action_type_notification">System Notification</string>
<string name="scene_select_socket">Select Target Socket</string> <string name="scene_select_socket">Select Target Socket</string>
<string name="scene_socket_power_state">Target Power State</string> <string name="scene_socket_power_state">Target Power State</string>
<string name="scene_select_virtual_remote">Select Virtual Remote</string> <string name="scene_select_virtual_remote">Select Virtual Remote</string>
@@ -144,8 +144,8 @@
<string name="scene_action_socket">Socket [%1$s]: Set %2$s</string> <string name="scene_action_socket">Socket [%1$s]: Set %2$s</string>
<string name="scene_action_remote">Remote [%1$s]: Send %2$s</string> <string name="scene_action_remote">Remote [%1$s]: Send %2$s</string>
<string name="scene_action_infrared">Infrared [%1$s]: Slot %2$s</string> <string name="scene_action_infrared">Infrared [%1$s]: Slot %2$s</string>
<string name="scene_action_delay">⏱️ Delay %1$d seconds</string> <string name="scene_action_delay">Delay %1$d seconds</string>
<string name="scene_action_notification">🔔 Notify: %1$s</string> <string name="scene_action_notification">Notify: %1$s</string>
<!-- Scene: execution --> <!-- Scene: execution -->
<string name="scene_execution_title">Execute Command</string> <string name="scene_execution_title">Execute Command</string>
@@ -164,13 +164,13 @@
<!-- Validation & dialog chips / placeholders --> <!-- Validation & dialog chips / placeholders -->
<string name="scene_trigger_time_error">Invalid time format (use HH:mm, e.g. 07:30)</string> <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_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_time">Schedule</string>
<string name="scene_chip_temp">🌡️ Temperature</string> <string name="scene_chip_temp">Temperature</string>
<string name="scene_chip_humidity">💧 Humidity</string> <string name="scene_chip_humidity">Humidity</string>
<string name="scene_chip_socket">🔌 Socket</string> <string name="scene_chip_socket">Socket</string>
<string name="scene_chip_remote">📺 Remote</string> <string name="scene_chip_remote">Remote</string>
<string name="scene_chip_delay">⏱️ Delay</string> <string name="scene_chip_delay">Delay</string>
<string name="scene_chip_notify">🔔 Notify</string> <string name="scene_chip_notify">Notify</string>
<string name="scene_chip_above">&gt; Above</string> <string name="scene_chip_above">&gt; Above</string>
<string name="scene_chip_below">&lt; Below</string> <string name="scene_chip_below">&lt; Below</string>
<string name="scene_chip_power_on">Turn ON</string> <string name="scene_chip_power_on">Turn ON</string>
@@ -184,7 +184,7 @@
<string name="scene_empty_triggers_hint">No triggers configured. Tap + below to add.</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_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_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_triggers_and_actions">%1$d triggers · %2$d actions</string>
<string name="scene_summary_actions_only">%1$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_ready_hint">Tap the button below to run immediately</string>
<string name="scene_exec_rerun">Re-run</string> <string name="scene_exec_rerun">Re-run</string>
@@ -57,6 +57,7 @@ dependencies {
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.test)
@@ -271,10 +271,10 @@ private fun VirtualRemoteCard(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
val icon = when (remote.type) { val icon = when (remote.type) {
RemoteDeviceType.TV -> "📺" RemoteDeviceType.TV -> "电视"
RemoteDeviceType.AIR_CONDITIONER -> "❄️" RemoteDeviceType.AIR_CONDITIONER -> "空调"
RemoteDeviceType.FAN -> "🌀" RemoteDeviceType.FAN -> "🌀"
RemoteDeviceType.CUSTOM -> "🎛️" RemoteDeviceType.CUSTOM -> "自定义"
} }
Text(icon, fontSize = 28.sp) Text(icon, fontSize = 28.sp)
IconButton(onClick = onDelete, modifier = Modifier.testTag("delete_remote_${remote.id.value}")) { IconButton(onClick = onDelete, modifier = Modifier.testTag("delete_remote_${remote.id.value}")) {
@@ -288,11 +288,11 @@ private fun VirtualRemoteCard(
) )
val modeText = if (remote.type == RemoteDeviceType.AIR_CONDITIONER) { val modeText = if (remote.type == RemoteDeviceType.AIR_CONDITIONER) {
"❄️ 状态机动态写码" "状态机动态写码"
} else if (remote.transmissionMode == TransmissionMode.STATIC_SLOTS) { } else if (remote.transmissionMode == TransmissionMode.STATIC_SLOTS) {
"🚀 静态独占槽位" "静态独占槽位"
} else { } else {
"📦 动态写码" "动态写码"
} }
Text( Text(
@@ -430,7 +430,7 @@ private fun CreateVirtualRemoteDialog(
OutlinedTextField( OutlinedTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("遥控器名称(如:客厅电视、我的遥控器)") }, label = { Text("遥控器名称(如:电视、我的遥控器)") },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth().testTag("create_remote_name_input"), modifier = Modifier.fillMaxWidth().testTag("create_remote_name_input"),
) )
@@ -443,10 +443,10 @@ private fun CreateVirtualRemoteDialog(
) { ) {
OutlinedTextField( OutlinedTextField(
value = when (selectedCustomLayout) { value = when (selectedCustomLayout) {
PanelLayoutStyle.TV_NAV -> "📺 电视导航布局 (内置方向盘/音量/频道)" PanelLayoutStyle.TV_NAV -> "电视导航布局(内置方向盘、音量、频道)"
PanelLayoutStyle.MEDIA_PLAYER -> "🎵 媒体播放器布局 (播放/快进/切歌)" PanelLayoutStyle.MEDIA_PLAYER -> "媒体播放器布局(播放、快进、切歌)"
PanelLayoutStyle.GRID -> "🎛️ 自由网格布局 (可自由增删按键)" PanelLayoutStyle.GRID -> "自由网格布局(可自由增删按键)"
PanelLayoutStyle.AC_COMPACT -> "❄️ 空调布局" PanelLayoutStyle.AC_COMPACT -> "空调布局"
}, },
onValueChange = {}, onValueChange = {},
readOnly = true, readOnly = true,
@@ -459,21 +459,21 @@ private fun CreateVirtualRemoteDialog(
onDismissRequest = { layoutDropdownExpanded = false }, onDismissRequest = { layoutDropdownExpanded = false },
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("📺 电视导航布局 (内置方向盘/音量/频道)") }, text = { Text("电视导航布局(内置方向盘、音量、频道)") },
onClick = { onClick = {
selectedCustomLayout = PanelLayoutStyle.TV_NAV selectedCustomLayout = PanelLayoutStyle.TV_NAV
layoutDropdownExpanded = false layoutDropdownExpanded = false
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("🎵 媒体播放器布局 (播放/快进/切歌)") }, text = { Text("媒体播放器布局(播放、快进、切歌)") },
onClick = { onClick = {
selectedCustomLayout = PanelLayoutStyle.MEDIA_PLAYER selectedCustomLayout = PanelLayoutStyle.MEDIA_PLAYER
layoutDropdownExpanded = false layoutDropdownExpanded = false
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("🎛️ 自由网格布局 (可自由增删按键)") }, text = { Text("自由网格布局(可自由增删按键)") },
onClick = { onClick = {
selectedCustomLayout = PanelLayoutStyle.GRID selectedCustomLayout = PanelLayoutStyle.GRID
layoutDropdownExpanded = false layoutDropdownExpanded = false
@@ -538,7 +538,7 @@ private fun CreateVirtualRemoteDialog(
onClick = { transmissionMode = TransmissionMode.STATIC_SLOTS }, onClick = { transmissionMode = TransmissionMode.STATIC_SLOTS },
) )
Column { Column {
Text("🚀 静态独占槽位模式", fontWeight = FontWeight.Medium) Text("静态独占槽位模式", fontWeight = FontWeight.Medium)
Text(stringResource(R.string.infrared_hub_trans_mode_static_desc), fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.infrared_hub_trans_mode_static_desc), fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
@@ -552,7 +552,7 @@ private fun CreateVirtualRemoteDialog(
onClick = { transmissionMode = TransmissionMode.DYNAMIC_SCRATCH }, onClick = { transmissionMode = TransmissionMode.DYNAMIC_SCRATCH },
) )
Column { Column {
Text("📦 动态写码模式", fontWeight = FontWeight.Medium) Text("动态写码模式", fontWeight = FontWeight.Medium)
Text(stringResource(R.string.infrared_hub_trans_mode_dynamic_desc), fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.infrared_hub_trans_mode_dynamic_desc), fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
@@ -563,7 +563,7 @@ private fun CreateVirtualRemoteDialog(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Text( Text(
text = "❄️ 空调多维状态自动采用 App 状态机 + 动态缓冲写码,杜绝状态踩踏并节省槽位。", text = "空调多维状态自动采用状态机和动态缓冲写码,避免状态冲突并节省槽位。",
fontSize = 12.sp, fontSize = 12.sp,
modifier = Modifier.padding(10.dp), modifier = Modifier.padding(10.dp),
color = MaterialTheme.colorScheme.onSecondaryContainer, color = MaterialTheme.colorScheme.onSecondaryContainer,
@@ -41,6 +41,16 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.material.icons.outlined.KeyboardArrowLeft
import androidx.compose.material.icons.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.KeyboardArrowUp
import androidx.compose.material.icons.outlined.PowerSettingsNew
import androidx.compose.material.icons.outlined.Remove
import androidx.compose.material.icons.outlined.VolumeOff
import androidx.compose.material.icons.outlined.Tv
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -333,7 +343,7 @@ private fun AcControlPanel(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = if (acState.power) "● 运行中" else "○ 已关机", text = if (acState.power) "运行中" else "已关机",
color = if (acState.power) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, color = if (acState.power) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
@@ -345,7 +355,7 @@ private fun AcControlPanel(
.background(if (acState.power) Color(0xFFFF5252) else MaterialTheme.colorScheme.outline) .background(if (acState.power) Color(0xFFFF5252) else MaterialTheme.colorScheme.outline)
.testTag("ac_power_toggle"), .testTag("ac_power_toggle"),
) { ) {
Text("⏻", color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold) Icon(Icons.Outlined.PowerSettingsNew, contentDescription = "电源", tint = Color.White)
} }
} }
@@ -371,14 +381,14 @@ private fun AcControlPanel(
modifier = Modifier.size(56.dp).testTag("ac_temp_down"), modifier = Modifier.size(56.dp).testTag("ac_temp_down"),
shape = CircleShape, shape = CircleShape,
) { ) {
Text("-", fontSize = 24.sp, fontWeight = FontWeight.Bold) Icon(Icons.Outlined.Remove, contentDescription = "降低温度")
} }
Button( Button(
onClick = { onAdjustTemp(1) }, onClick = { onAdjustTemp(1) },
modifier = Modifier.size(56.dp).testTag("ac_temp_up"), modifier = Modifier.size(56.dp).testTag("ac_temp_up"),
shape = CircleShape, shape = CircleShape,
) { ) {
Text("+", fontSize = 24.sp, fontWeight = FontWeight.Bold) Icon(Icons.Outlined.Add, contentDescription = "升高温度")
} }
} }
} }
@@ -421,19 +431,19 @@ private fun AcControlPanel(
FilterChip( FilterChip(
selected = acState.verticalSwing, selected = acState.verticalSwing,
onClick = onToggleVSwing, onClick = onToggleVSwing,
label = { Text("↕ 上下扫风") }, label = { Text("上下扫风") },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
FilterChip( FilterChip(
selected = acState.horizontalSwing, selected = acState.horizontalSwing,
onClick = onToggleHSwing, onClick = onToggleHSwing,
label = { Text("↔ 左右扫风") }, label = { Text("左右扫风") },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
FilterChip( FilterChip(
selected = acState.eco, selected = acState.eco,
onClick = onToggleEco, onClick = onToggleEco,
label = { Text("🌱 节能 ECO") }, label = { Text("节能") },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
@@ -463,9 +473,9 @@ private fun TvControlPanel(
horizontalArrangement = Arrangement.SpaceAround, horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
InteractiveRemoteButton("TV_POWER", "⏻ 电源", Color(0xFFFF5252), bindingMap["TV_POWER"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("TV_POWER", "电源", Color(0xFFFF5252), bindingMap["TV_POWER"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
InteractiveRemoteButton("TV_MUTE", "🔇 静音", MaterialTheme.colorScheme.secondary, bindingMap["TV_MUTE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("TV_MUTE", "静音", MaterialTheme.colorScheme.secondary, bindingMap["TV_MUTE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
InteractiveRemoteButton("TV_SOURCE", "📺 信号源", MaterialTheme.colorScheme.secondary, bindingMap["TV_SOURCE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("TV_SOURCE", "信号源", MaterialTheme.colorScheme.secondary, bindingMap["TV_SOURCE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -482,7 +492,7 @@ private fun TvControlPanel(
onClick = { onButtonClick("TV_NAV_UP") }, onClick = { onButtonClick("TV_NAV_UP") },
modifier = Modifier.align(Alignment.TopCenter).padding(top = 10.dp).testTag("tv_nav_up"), modifier = Modifier.align(Alignment.TopCenter).padding(top = 10.dp).testTag("tv_nav_up"),
) { ) {
ButtonIndicatorText("TV_NAV_UP", "▲", learningButtonId, sendingButtonId) ButtonIndicatorText("TV_NAV_UP", "上", learningButtonId, sendingButtonId)
} }
// 下 // 下
@@ -490,7 +500,7 @@ private fun TvControlPanel(
onClick = { onButtonClick("TV_NAV_DOWN") }, onClick = { onButtonClick("TV_NAV_DOWN") },
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 10.dp).testTag("tv_nav_down"), modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 10.dp).testTag("tv_nav_down"),
) { ) {
ButtonIndicatorText("TV_NAV_DOWN", "▼", learningButtonId, sendingButtonId) ButtonIndicatorText("TV_NAV_DOWN", "下", learningButtonId, sendingButtonId)
} }
// 左 // 左
@@ -498,7 +508,7 @@ private fun TvControlPanel(
onClick = { onButtonClick("TV_NAV_LEFT") }, onClick = { onButtonClick("TV_NAV_LEFT") },
modifier = Modifier.align(Alignment.CenterStart).padding(start = 10.dp).testTag("tv_nav_left"), modifier = Modifier.align(Alignment.CenterStart).padding(start = 10.dp).testTag("tv_nav_left"),
) { ) {
ButtonIndicatorText("TV_NAV_LEFT", "◀", learningButtonId, sendingButtonId) ButtonIndicatorText("TV_NAV_LEFT", "左", learningButtonId, sendingButtonId)
} }
// 右 // 右
@@ -506,7 +516,7 @@ private fun TvControlPanel(
onClick = { onButtonClick("TV_NAV_RIGHT") }, onClick = { onButtonClick("TV_NAV_RIGHT") },
modifier = Modifier.align(Alignment.CenterEnd).padding(end = 10.dp).testTag("tv_nav_right"), modifier = Modifier.align(Alignment.CenterEnd).padding(end = 10.dp).testTag("tv_nav_right"),
) { ) {
ButtonIndicatorText("TV_NAV_RIGHT", "▶", learningButtonId, sendingButtonId) ButtonIndicatorText("TV_NAV_RIGHT", "右", learningButtonId, sendingButtonId)
} }
// OK 居中大按钮 // OK 居中大按钮
@@ -554,10 +564,10 @@ private fun TvControlPanel(
) { ) {
Text("频道 CH", fontWeight = FontWeight.Bold, fontSize = 12.sp) Text("频道 CH", fontWeight = FontWeight.Bold, fontSize = 12.sp)
Button(onClick = { onButtonClick("TV_CH_UP") }, modifier = Modifier.size(54.dp), shape = RoundedCornerShape(12.dp)) { Button(onClick = { onButtonClick("TV_CH_UP") }, modifier = Modifier.size(54.dp), shape = RoundedCornerShape(12.dp)) {
ButtonIndicatorText("TV_CH_UP", "▲", learningButtonId, sendingButtonId) ButtonIndicatorText("TV_CH_UP", "频道上调", learningButtonId, sendingButtonId)
} }
Button(onClick = { onButtonClick("TV_CH_DOWN") }, modifier = Modifier.size(54.dp), shape = RoundedCornerShape(12.dp)) { Button(onClick = { onButtonClick("TV_CH_DOWN") }, modifier = Modifier.size(54.dp), shape = RoundedCornerShape(12.dp)) {
ButtonIndicatorText("TV_CH_DOWN", "▼", learningButtonId, sendingButtonId) ButtonIndicatorText("TV_CH_DOWN", "频道下调", learningButtonId, sendingButtonId)
} }
} }
} }
@@ -583,8 +593,8 @@ private fun MediaPlayerControlPanel(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
InteractiveRemoteButton("MEDIA_POWER", "⏻ 电源", Color(0xFFFF5252), bindingMap["MEDIA_POWER"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("MEDIA_POWER", "电源", Color(0xFFFF5252), bindingMap["MEDIA_POWER"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
InteractiveRemoteButton("MEDIA_MUTE", "🔇 静音", MaterialTheme.colorScheme.secondary, bindingMap["MEDIA_MUTE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("MEDIA_MUTE", "静音", MaterialTheme.colorScheme.secondary, bindingMap["MEDIA_MUTE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -642,13 +652,13 @@ private fun GenericGridControlPanel(
) { ) {
Text("按键列表 (${bindings.size})", fontWeight = FontWeight.Bold, fontSize = 16.sp) Text("按键列表 (${bindings.size})", fontWeight = FontWeight.Bold, fontSize = 16.sp)
Button(onClick = onAddButton, modifier = Modifier.testTag("grid_add_key_button")) { Button(onClick = onAddButton, modifier = Modifier.testTag("grid_add_key_button")) {
Text("➕ 添加按键") Text("添加按键")
} }
} }
if (bindings.isEmpty()) { if (bindings.isEmpty()) {
Box(modifier = Modifier.fillMaxWidth().weight(1f), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxWidth().weight(1f), contentAlignment = Alignment.Center) {
Text("暂无自定义按键,点击上方【➕ 添加按键】新建", color = MaterialTheme.colorScheme.onSurfaceVariant) Text("暂无自定义按键,请使用上方添加操作新建", color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
} else { } else {
LazyVerticalGrid( LazyVerticalGrid(
@@ -697,9 +707,9 @@ private fun GridButtonCard(
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(binding.displayName, fontWeight = FontWeight.Bold, fontSize = 15.sp) Text(binding.displayName, fontWeight = FontWeight.Bold, fontSize = 15.sp)
val statusText = when { val statusText = when {
isLearning -> "🟡 正在录制信号..." isLearning -> "正在录制信号..."
binding.status == InfraredKeyStatus.READY -> "🟢 已就绪 (Slot ${binding.assignedSlot ?: "动态"})" binding.status == InfraredKeyStatus.READY -> "已就绪(槽位 ${binding.assignedSlot ?: "动态"})"
else -> "⚪ 待录制 (点击录码)" else -> "待录制(点击录码)"
} }
Text(statusText, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(statusText, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
@@ -712,7 +722,7 @@ private fun GridButtonCard(
onClick = onDelete, onClick = onDelete,
modifier = Modifier.size(36.dp).testTag("delete_key_${binding.buttonId}"), modifier = Modifier.size(36.dp).testTag("delete_key_${binding.buttonId}"),
) { ) {
Text("🗑️", fontSize = 15.sp) Text("删除", fontSize = 12.sp)
} }
} }
} }
@@ -756,7 +766,7 @@ private fun ButtonIndicatorText(
sendingButtonId: String?, sendingButtonId: String?,
) { ) {
if (learningButtonId == buttonId) { if (learningButtonId == buttonId) {
Text("🟡 录制中", fontSize = 12.sp, fontWeight = FontWeight.Bold) Text("录制中", fontSize = 12.sp, fontWeight = FontWeight.Bold)
} else if (sendingButtonId == buttonId) { } else if (sendingButtonId == buttonId) {
CircularProgressIndicator(modifier = Modifier.size(16.dp), color = Color.White) CircularProgressIndicator(modifier = Modifier.size(16.dp), color = Color.White)
} else { } else {
@@ -26,24 +26,24 @@ data class RemoteButtonSpec(
object RemoteSceneConfig { object RemoteSceneConfig {
object AirConditioner { object AirConditioner {
val POWER = RemoteButtonSpec("AC_POWER", "1", R.string.infrared_key_ac_power, "空调-电源", "⏻") val POWER = RemoteButtonSpec("AC_POWER", "1", R.string.infrared_key_ac_power, "空调-电源", "电源")
val TEMP_UP = RemoteButtonSpec("AC_TEMP_UP", "2", R.string.infrared_key_ac_temp_up, "空调-温度+", "▲") val TEMP_UP = RemoteButtonSpec("AC_TEMP_UP", "2", R.string.infrared_key_ac_temp_up, "空调-温度+", "升温")
val TEMP_DOWN = RemoteButtonSpec("AC_TEMP_DOWN", "3", R.string.infrared_key_ac_temp_down, "空调-温度-", "▼") val TEMP_DOWN = RemoteButtonSpec("AC_TEMP_DOWN", "3", R.string.infrared_key_ac_temp_down, "空调-温度-", "降温")
val MODE = RemoteButtonSpec("AC_MODE", "4", R.string.infrared_key_ac_mode, "空调-模式", "模式 ❄/☀") val MODE = RemoteButtonSpec("AC_MODE", "4", R.string.infrared_key_ac_mode, "空调-模式", "模式")
val FAN = RemoteButtonSpec("AC_FAN", "5", R.string.infrared_key_ac_fan, "空调-风速", "风速 💨") val FAN = RemoteButtonSpec("AC_FAN", "5", R.string.infrared_key_ac_fan, "空调-风速", "风速")
val SWING = RemoteButtonSpec("AC_SWING", "6", R.string.infrared_key_ac_swing, "空调-扫风", "扫风 ↔") val SWING = RemoteButtonSpec("AC_SWING", "6", R.string.infrared_key_ac_swing, "空调-扫风", "扫风")
val TIMER = RemoteButtonSpec("AC_TIMER", "7", R.string.infrared_key_ac_timer, "空调-定时", "定时 ⏱") val TIMER = RemoteButtonSpec("AC_TIMER", "7", R.string.infrared_key_ac_timer, "空调-定时", "定时")
val buttons = listOf(POWER, TEMP_UP, TEMP_DOWN, MODE, FAN, SWING, TIMER) val buttons = listOf(POWER, TEMP_UP, TEMP_DOWN, MODE, FAN, SWING, TIMER)
} }
object Television { object Television {
val POWER = RemoteButtonSpec("TV_POWER", "21", R.string.infrared_key_tv_power, "电视-电源", "⏻") val POWER = RemoteButtonSpec("TV_POWER", "21", R.string.infrared_key_tv_power, "电视-电源", "电源")
val MUTE = RemoteButtonSpec("TV_MUTE", "22", R.string.infrared_key_tv_mute, "电视-静音", "🔇") val MUTE = RemoteButtonSpec("TV_MUTE", "22", R.string.infrared_key_tv_mute, "电视-静音", "静音")
val NAV_UP = RemoteButtonSpec("TV_NAV_UP", "23", R.string.infrared_key_tv_nav_up, "电视-上", "▲") val NAV_UP = RemoteButtonSpec("TV_NAV_UP", "23", R.string.infrared_key_tv_nav_up, "电视-上", "上")
val NAV_DOWN = RemoteButtonSpec("TV_NAV_DOWN", "24", R.string.infrared_key_tv_nav_down, "电视-下", "▼") val NAV_DOWN = RemoteButtonSpec("TV_NAV_DOWN", "24", R.string.infrared_key_tv_nav_down, "电视-下", "下")
val NAV_LEFT = RemoteButtonSpec("TV_NAV_LEFT", "25", R.string.infrared_key_tv_nav_left, "电视-左", "◀") val NAV_LEFT = RemoteButtonSpec("TV_NAV_LEFT", "25", R.string.infrared_key_tv_nav_left, "电视-左", "左")
val NAV_RIGHT = RemoteButtonSpec("TV_NAV_RIGHT", "26", R.string.infrared_key_tv_nav_right, "电视-右", "▶") val NAV_RIGHT = RemoteButtonSpec("TV_NAV_RIGHT", "26", R.string.infrared_key_tv_nav_right, "电视-右", "右")
val OK = RemoteButtonSpec("TV_OK", "27", R.string.infrared_key_tv_ok, "电视-确定", "OK") val OK = RemoteButtonSpec("TV_OK", "27", R.string.infrared_key_tv_ok, "电视-确定", "OK")
val VOL_UP = RemoteButtonSpec("TV_VOL_UP", "28", R.string.infrared_key_tv_vol_up, "电视-音量+", "VOL +") val VOL_UP = RemoteButtonSpec("TV_VOL_UP", "28", R.string.infrared_key_tv_vol_up, "电视-音量+", "VOL +")
val VOL_DOWN = RemoteButtonSpec("TV_VOL_DOWN", "29", R.string.infrared_key_tv_vol_down, "电视-音量-", "VOL -") val VOL_DOWN = RemoteButtonSpec("TV_VOL_DOWN", "29", R.string.infrared_key_tv_vol_down, "电视-音量-", "VOL -")
@@ -45,6 +45,8 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.designsystem.ABoxSurface import com.flagship.abox.manager.designsystem.ABoxSurface
import com.flagship.abox.manager.designsystem.ABoxIconButton
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
import com.flagship.abox.manager.infrared.R import com.flagship.abox.manager.infrared.R
import com.flagship.abox.manager.infrared.remote.InfraredRemoteUiState import com.flagship.abox.manager.infrared.remote.InfraredRemoteUiState
import com.flagship.abox.manager.infrared.remote.InfraredRemoteViewModel import com.flagship.abox.manager.infrared.remote.InfraredRemoteViewModel
@@ -158,12 +160,12 @@ internal fun InfraredRemoteScreen(
} }
// 按键管理 / 设置入口按钮 // 按键管理 / 设置入口按钮
IconButton( ABoxIconButton(
key = ABoxIconKey.EDIT,
contentDescription = "打开红外设置",
onClick = onOpenSettings, onClick = onOpenSettings,
modifier = Modifier.testTag("remote_settings_button"), modifier = Modifier.testTag("remote_settings_button"),
) { )
Text("⚙️", fontSize = 20.sp)
}
}, },
) )
}, },
@@ -35,12 +35,12 @@
<string name="infrared_remote_title">红外遥控器</string> <string name="infrared_remote_title">红外遥控器</string>
<string name="infrared_remote_recording">录制中</string> <string name="infrared_remote_recording">录制中</string>
<string name="infrared_remote_learning_mode">学习模式</string> <string name="infrared_remote_learning_mode">学习模式</string>
<string name="infrared_remote_learning_banner">🔴 学习模式已开启:点击任意按键开始录制实体遥控器信号</string> <string name="infrared_remote_learning_banner">学习模式已开启:点击任意按键开始录制实体遥控器信号</string>
<string name="infrared_remote_scene_ac">❄️ 空调面板</string> <string name="infrared_remote_scene_ac">空调面板</string>
<string name="infrared_remote_scene_tv">📺 电视面板</string> <string name="infrared_remote_scene_tv">电视面板</string>
<string name="infrared_remote_settings">设置</string> <string name="infrared_remote_settings">设置</string>
<string name="infrared_msg_learning_mode_enabled">🔴 学习模式已开启:点击任意按键开始录制</string> <string name="infrared_msg_learning_mode_enabled">学习模式已开启:点击任意按键开始录制</string>
<string name="infrared_msg_recording_key">正在录制【%1$s】(槽位 %2$s),请按下实体遥控器按键…</string> <string name="infrared_msg_recording_key">正在录制【%1$s】(槽位 %2$s),请按下实体遥控器按键…</string>
<string name="infrared_msg_learned_success">【%1$s】录制成功</string> <string name="infrared_msg_learned_success">【%1$s】录制成功</string>
<string name="infrared_msg_key_not_learned">按键【%1$s】尚未学习,开启右上角学习模式即可录制</string> <string name="infrared_msg_key_not_learned">按键【%1$s】尚未学习,开启右上角学习模式即可录制</string>
@@ -51,8 +51,8 @@
<string name="infrared_label_temp_control">温度调节</string> <string name="infrared_label_temp_control">温度调节</string>
<string name="infrared_label_vol">音量</string> <string name="infrared_label_vol">音量</string>
<string name="infrared_label_ch">频道</string> <string name="infrared_label_ch">频道</string>
<string name="infrared_label_cool_mode">❄️ 制冷模式</string> <string name="infrared_label_cool_mode">制冷模式</string>
<string name="infrared_label_ac_status">💨 自动风速 | ↔ 自动扫风</string> <string name="infrared_label_ac_status">自动风速 | 自动扫风</string>
<string name="infrared_key_ac_power">空调-电源</string> <string name="infrared_key_ac_power">空调-电源</string>
<string name="infrared_key_ac_temp_up">空调-升温</string> <string name="infrared_key_ac_temp_up">空调-升温</string>
@@ -98,10 +98,10 @@
<string name="infrared_hub_static_quota">静态占用:%1$d</string> <string name="infrared_hub_static_quota">静态占用:%1$d</string>
<string name="infrared_hub_dynamic_scratch">动态缓冲:Slot %1$s</string> <string name="infrared_hub_dynamic_scratch">动态缓冲:Slot %1$s</string>
<string name="infrared_hub_remotes_title">虚拟遥控器</string> <string name="infrared_hub_remotes_title">虚拟遥控器</string>
<string name="infrared_hub_no_remotes">暂无虚拟遥控器,点击右上角或下方 ➕ 添加</string> <string name="infrared_hub_no_remotes">暂无虚拟遥控器,请使用添加操作</string>
<string name="infrared_hub_add_remote">添加遥控器</string> <string name="infrared_hub_add_remote">添加遥控器</string>
<string name="infrared_hub_mode_static">🚀 静态独占(%1$s 槽位)</string> <string name="infrared_hub_mode_static">静态独占(%1$s 槽位)</string>
<string name="infrared_hub_mode_dynamic">📦 动态缓冲(共用 Slot %1$s)</string> <string name="infrared_hub_mode_dynamic">动态缓冲(共用槽位 %1$s)</string>
<string name="infrared_hub_create_dialog_title">添加虚拟遥控器</string> <string name="infrared_hub_create_dialog_title">添加虚拟遥控器</string>
<string name="infrared_hub_device_type_label">设备类型</string> <string name="infrared_hub_device_type_label">设备类型</string>
<string name="infrared_hub_brand_label">预设码库 / 品牌</string> <string name="infrared_hub_brand_label">预设码库 / 品牌</string>
@@ -110,8 +110,8 @@
<string name="infrared_hub_trans_mode_dynamic_desc">节省槽位(0 额外槽位),现场写码发射(~80ms)</string> <string name="infrared_hub_trans_mode_dynamic_desc">节省槽位(0 额外槽位),现场写码发射(~80ms)</string>
<string name="infrared_hub_quota_insufficient">硬件槽位不足,请选择动态写码模式</string> <string name="infrared_hub_quota_insufficient">硬件槽位不足,请选择动态写码模式</string>
<string name="infrared_hub_raw_keys_button">硬件槽位与底层按键管理</string> <string name="infrared_hub_raw_keys_button">硬件槽位与底层按键管理</string>
<string name="infrared_hub_device_type_tv">📺 电视机 / 电视盒子</string> <string name="infrared_hub_device_type_tv">电视机 / 电视盒子</string>
<string name="infrared_hub_device_type_ac">❄️ 空调设备</string> <string name="infrared_hub_device_type_ac">空调设备</string>
<string name="infrared_hub_device_type_fan">🌀 电风扇 / 空气净化器</string> <string name="infrared_hub_device_type_fan">🌀 电风扇 / 空气净化器</string>
<string name="infrared_hub_device_type_custom">🎛️ 自定义面板</string> <string name="infrared_hub_device_type_custom">自定义面板</string>
</resources> </resources>
@@ -36,12 +36,12 @@
<string name="infrared_remote_recording">Recording</string> <string name="infrared_remote_recording">Recording</string>
<string name="infrared_hub_my_remotes">My Remotes</string> <string name="infrared_hub_my_remotes">My Remotes</string>
<string name="infrared_remote_learning_mode">Learn Mode</string> <string name="infrared_remote_learning_mode">Learn Mode</string>
<string name="infrared_remote_learning_banner">🔴 Learn mode active: tap any button to record</string> <string name="infrared_remote_learning_banner">Learn mode active: tap any button to record</string>
<string name="infrared_remote_scene_ac">❄️ Air Conditioner</string> <string name="infrared_remote_scene_ac">Air Conditioner</string>
<string name="infrared_remote_scene_tv">📺 Television</string> <string name="infrared_remote_scene_tv">Television</string>
<string name="infrared_remote_settings">Settings</string> <string name="infrared_remote_settings">Settings</string>
<string name="infrared_msg_learning_mode_enabled">🔴 Learn mode active: tap any button to record</string> <string name="infrared_msg_learning_mode_enabled">Learn mode active: tap any button to record</string>
<string name="infrared_msg_recording_key">Recording [%1$s] (Slot %2$s), press remote button…</string> <string name="infrared_msg_recording_key">Recording [%1$s] (Slot %2$s), press remote button…</string>
<string name="infrared_msg_learned_success">[%1$s] recorded successfully</string> <string name="infrared_msg_learned_success">[%1$s] recorded successfully</string>
<string name="infrared_msg_key_not_learned">Button [%1$s] is not learned. Enable learn mode to record.</string> <string name="infrared_msg_key_not_learned">Button [%1$s] is not learned. Enable learn mode to record.</string>
@@ -52,8 +52,8 @@
<string name="infrared_label_temp_control">Temp Control</string> <string name="infrared_label_temp_control">Temp Control</string>
<string name="infrared_label_vol">VOL</string> <string name="infrared_label_vol">VOL</string>
<string name="infrared_label_ch">CH</string> <string name="infrared_label_ch">CH</string>
<string name="infrared_label_cool_mode">❄️ Cool Mode</string> <string name="infrared_label_cool_mode">Cool Mode</string>
<string name="infrared_label_ac_status">💨 Auto Fan | ↔ Swing</string> <string name="infrared_label_ac_status">Auto Fan | Swing</string>
<string name="infrared_key_ac_power">AC Power</string> <string name="infrared_key_ac_power">AC Power</string>
<string name="infrared_key_ac_temp_up">AC Temp +</string> <string name="infrared_key_ac_temp_up">AC Temp +</string>
@@ -100,8 +100,8 @@
<string name="infrared_hub_remotes_title">Virtual Remotes</string> <string name="infrared_hub_remotes_title">Virtual Remotes</string>
<string name="infrared_hub_no_remotes">No virtual remotes added yet. Tap + to add one.</string> <string name="infrared_hub_no_remotes">No virtual remotes added yet. Tap + to add one.</string>
<string name="infrared_hub_add_remote">Add Remote</string> <string name="infrared_hub_add_remote">Add Remote</string>
<string name="infrared_hub_mode_static">🚀 Static Mode (%1$s slots)</string> <string name="infrared_hub_mode_static">Static Mode (%1$s slots)</string>
<string name="infrared_hub_mode_dynamic">📦 Dynamic Mode (Scratch Slot %1$s)</string> <string name="infrared_hub_mode_dynamic">Dynamic Mode (Scratch Slot %1$s)</string>
<string name="infrared_hub_create_dialog_title">Add Virtual Remote</string> <string name="infrared_hub_create_dialog_title">Add Virtual Remote</string>
<string name="infrared_hub_device_type_label">Device Type</string> <string name="infrared_hub_device_type_label">Device Type</string>
<string name="infrared_hub_brand_label">Brand / Preset Library</string> <string name="infrared_hub_brand_label">Brand / Preset Library</string>
@@ -110,8 +110,8 @@
<string name="infrared_hub_trans_mode_dynamic_desc">Saves slots (0 extra slots), writes code on-the-fly (~80ms)</string> <string name="infrared_hub_trans_mode_dynamic_desc">Saves slots (0 extra slots), writes code on-the-fly (~80ms)</string>
<string name="infrared_hub_quota_insufficient">Slot quota exceeded. Please choose Dynamic mode.</string> <string name="infrared_hub_quota_insufficient">Slot quota exceeded. Please choose Dynamic mode.</string>
<string name="infrared_hub_raw_keys_button">Hardware Slot / Raw Key Management</string> <string name="infrared_hub_raw_keys_button">Hardware Slot / Raw Key Management</string>
<string name="infrared_hub_device_type_tv">📺 Television (TV)</string> <string name="infrared_hub_device_type_tv">Television (TV)</string>
<string name="infrared_hub_device_type_ac">❄️ Air Conditioner (AC)</string> <string name="infrared_hub_device_type_ac">Air Conditioner (AC)</string>
<string name="infrared_hub_device_type_fan">🌀 Fan / Air Purifier</string> <string name="infrared_hub_device_type_fan">🌀 Fan / Air Purifier</string>
<string name="infrared_hub_device_type_custom">🎛️ Custom Panel</string> <string name="infrared_hub_device_type_custom">Custom Panel</string>
</resources> </resources>
@@ -12,6 +12,7 @@ import com.flagship.abox.manager.domain.infrared.VirtualRemoteRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class FakeVirtualRemoteRepository : VirtualRemoteRepository { class FakeVirtualRemoteRepository : VirtualRemoteRepository {
@@ -80,13 +81,13 @@ class FakeVirtualRemoteRepository : VirtualRemoteRepository {
): DomainResult<VirtualRemoteId> { ): DomainResult<VirtualRemoteId> {
remotes[remote.id] = remote remotes[remote.id] = remote
bindings[remote.id] = initialBindings.toMutableList() bindings[remote.id] = initialBindings.toMutableList()
changeTrigger.value = System.currentTimeMillis() changeTrigger.update { it + 1L }
return DomainResult.Success(remote.id) return DomainResult.Success(remote.id)
} }
override suspend fun updateRemote(remote: VirtualRemote): DomainResult<Unit> { override suspend fun updateRemote(remote: VirtualRemote): DomainResult<Unit> {
remotes[remote.id] = remote remotes[remote.id] = remote
changeTrigger.value = System.currentTimeMillis() changeTrigger.update { it + 1L }
return DomainResult.Success(Unit) return DomainResult.Success(Unit)
} }
@@ -96,7 +97,7 @@ class FakeVirtualRemoteRepository : VirtualRemoteRepository {
): DomainResult<Unit> { ): DomainResult<Unit> {
remotes.remove(remoteId) remotes.remove(remoteId)
bindings.remove(remoteId) bindings.remove(remoteId)
changeTrigger.value = System.currentTimeMillis() changeTrigger.update { it + 1L }
return DomainResult.Success(Unit) return DomainResult.Success(Unit)
} }
@@ -109,7 +110,7 @@ class FakeVirtualRemoteRepository : VirtualRemoteRepository {
list.add(binding) list.add(binding)
} }
bindings[binding.remoteId] = list bindings[binding.remoteId] = list
changeTrigger.value = System.currentTimeMillis() changeTrigger.update { it + 1L }
return DomainResult.Success(Unit) return DomainResult.Success(Unit)
} }
@@ -120,7 +121,7 @@ class FakeVirtualRemoteRepository : VirtualRemoteRepository {
val list = bindings[remoteId] ?: return DomainResult.Success(Unit) val list = bindings[remoteId] ?: return DomainResult.Success(Unit)
list.removeAll { it.buttonId == buttonId } list.removeAll { it.buttonId == buttonId }
bindings[remoteId] = list bindings[remoteId] = list
changeTrigger.value = System.currentTimeMillis() changeTrigger.update { it + 1L }
return DomainResult.Success(Unit) return DomainResult.Success(Unit)
} }
@@ -131,7 +132,7 @@ class FakeVirtualRemoteRepository : VirtualRemoteRepository {
): DomainResult<Unit> { ): DomainResult<Unit> {
val existing = remotes[remoteId] ?: return DomainResult.Success(Unit) val existing = remotes[remoteId] ?: return DomainResult.Success(Unit)
remotes[remoteId] = existing.copy(acState = state) remotes[remoteId] = existing.copy(acState = state)
changeTrigger.value = System.currentTimeMillis() changeTrigger.update { it + 1L }
return DomainResult.Success(Unit) return DomainResult.Success(Unit)
} }
} }