From ec51ae1db73403e2b91ce37bc6bac3c4ee10e98a Mon Sep 17 00:00:00 2001
From: KremeCN <3265494849@qq.com>
Date: Tue, 25 Aug 2026 14:34:47 +0800
Subject: [PATCH] feat(infrared): choose layout template at creation time and
improve grid key management
---
.../manager/infrared/hub/InfraredHubScreen.kt | 260 ++++++++++++------
.../infrared/hub/InfraredHubViewModel.kt | 2 +
.../infrared/panel/RemotePanelScreen.kt | 134 ++++-----
.../src/main/res/values-zh-rCN/strings.xml | 1 +
.../infrared/src/main/res/values/strings.xml | 1 +
.../infrared/hub/InfraredHubViewModelTest.kt | 27 ++
.../domain/infrared/CreateVirtualRemote.kt | 26 +-
.../domain/infrared/PresetCodeLibrary.kt | 72 +++--
8 files changed, 321 insertions(+), 202 deletions(-)
diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt
index 4498cc8..1ee37cb 100644
--- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt
+++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt
@@ -1,5 +1,6 @@
package com.flagship.abox.manager.infrared.hub
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -10,19 +11,23 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.Icon
+import androidx.compose.material3.FilterChip
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
@@ -31,6 +36,7 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -42,6 +48,8 @@ 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.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
@@ -51,10 +59,11 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.designsystem.ABoxSurface
-import com.flagship.abox.manager.domain.ProviderType
-import com.flagship.abox.manager.domain.infrared.PresetBrandOption
+import com.flagship.abox.manager.domain.ProviderProfileId
+import com.flagship.abox.manager.domain.infrared.PanelLayoutStyle
import com.flagship.abox.manager.domain.infrared.PresetCodeLibrary
import com.flagship.abox.manager.domain.infrared.RemoteDeviceType
+import com.flagship.abox.manager.domain.infrared.SlotQuotaSummary
import com.flagship.abox.manager.domain.infrared.TransmissionMode
import com.flagship.abox.manager.domain.infrared.VirtualRemote
import com.flagship.abox.manager.domain.infrared.VirtualRemoteId
@@ -74,12 +83,11 @@ fun InfraredHubRoute(
InfraredHubScreen(
state = state,
onBack = onBack,
+ onNavigateToSettings = onOpenSettings,
onOpenRemote = onOpenRemote,
- onOpenSettings = onOpenSettings,
- onSessionInvalid = onSessionInvalid,
- onCreateRemote = viewModel::createRemote,
onDeleteRemote = viewModel::deleteRemote,
- onSaveDeviceName = viewModel::saveDeviceName,
+ onCreateRemote = viewModel::createRemote,
+ onSessionInvalid = onSessionInvalid,
onMessageShown = viewModel::clearMessage,
)
}
@@ -89,12 +97,11 @@ fun InfraredHubRoute(
internal fun InfraredHubScreen(
state: InfraredHubUiState,
onBack: () -> Unit,
+ onNavigateToSettings: () -> Unit,
onOpenRemote: (String) -> Unit,
- onOpenSettings: () -> Unit,
- onSessionInvalid: () -> Unit,
- onCreateRemote: (String, RemoteDeviceType, String?, TransmissionMode) -> Unit,
onDeleteRemote: (VirtualRemoteId) -> Unit,
- onSaveDeviceName: (String) -> Unit,
+ onCreateRemote: (String, RemoteDeviceType, String?, TransmissionMode, PanelLayoutStyle?) -> Unit,
+ onSessionInvalid: () -> Unit,
onMessageShown: () -> Unit,
) {
val snackbarHostState = remember { SnackbarHostState() }
@@ -131,7 +138,21 @@ internal fun InfraredHubScreen(
Scaffold(
topBar = {
TopAppBar(
- title = { Text(stringResource(R.string.infrared_hub_title), fontWeight = FontWeight.Bold) },
+ title = {
+ Column {
+ Text(
+ text = stringResource(R.string.infrared_hub_title),
+ fontWeight = FontWeight.Bold,
+ )
+ state.providerDisplayName?.let {
+ Text(
+ text = it,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ },
navigationIcon = {
TextButton(onClick = onBack) {
Text(stringResource(R.string.infrared_back))
@@ -139,7 +160,7 @@ internal fun InfraredHubScreen(
},
actions = {
IconButton(
- onClick = onOpenSettings,
+ onClick = onNavigateToSettings,
modifier = Modifier.testTag("hub_settings_button"),
) {
Text("⚙️", fontSize = 20.sp)
@@ -160,71 +181,20 @@ internal fun InfraredHubScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
- // 1. 顶部环境与 100 槽位看板卡片
- Card(modifier = Modifier.fillMaxWidth()) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(10.dp),
- ) {
- val envText = when (state.providerType) {
- ProviderType.LOCAL_EXPERIENCE -> stringResource(R.string.infrared_mode_local_simulation)
- ProviderType.ABOX -> stringResource(R.string.infrared_mode_abox_real)
- null -> state.profileId?.value ?: ""
- }
- Text(
- text = stringResource(R.string.infrared_mode_label, envText),
- style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
- color = MaterialTheme.colorScheme.primary,
- )
+ // 1. 硬件槽位配额看板 (100 槽位预算)
+ QuotaSummaryCard(quota = state.slotQuota)
- Text(
- text = stringResource(R.string.infrared_saved_device, state.savedDeviceName.value),
- style = MaterialTheme.typography.bodyMedium,
- )
-
- // 槽位配额指示条
- val progress = state.slotQuota.usedCount.toFloat() / state.slotQuota.totalCapacity
- LinearProgressIndicator(
- progress = { progress.coerceIn(0f, 1f) },
- modifier = Modifier.fillMaxWidth().height(8.dp),
- )
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- Text(
- text = stringResource(
- R.string.infrared_hub_slot_quota,
- state.slotQuota.usedCount,
- state.slotQuota.totalCapacity,
- state.slotQuota.remainingStaticCount,
- ),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- Text(
- text = stringResource(R.string.infrared_hub_dynamic_scratch, "100"),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.tertiary,
- fontWeight = FontWeight.Medium,
- )
- }
- }
- }
-
- // 2. 虚拟遥控器列表标题栏
+ // 2. 遥控器列表标题与新建按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- text = stringResource(R.string.infrared_hub_remotes_title),
- style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
+ text = stringResource(R.string.infrared_hub_my_remotes),
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
)
+
Button(
onClick = { showCreateDialog = true },
modifier = Modifier.testTag("hub_add_remote_button"),
@@ -268,8 +238,8 @@ internal fun InfraredHubScreen(
if (showCreateDialog) {
CreateVirtualRemoteDialog(
onDismiss = { showCreateDialog = false },
- onConfirm = { name, type, brandModel, transMode ->
- onCreateRemote(name, type, brandModel, transMode)
+ onConfirm = { name, type, brandModel, transMode, layoutStyle ->
+ onCreateRemote(name, type, brandModel, transMode, layoutStyle)
showCreateDialog = false
},
)
@@ -318,14 +288,65 @@ private fun VirtualRemoteCard(
)
val modeText = if (remote.type == RemoteDeviceType.AIR_CONDITIONER) {
- stringResource(R.string.infrared_hub_mode_dynamic, "100")
- } else when (remote.transmissionMode) {
- TransmissionMode.STATIC_SLOTS -> stringResource(R.string.infrared_hub_mode_static, "15")
- TransmissionMode.DYNAMIC_SCRATCH -> stringResource(R.string.infrared_hub_mode_dynamic, "100")
+ "❄️ 状态机动态写码"
+ } else if (remote.transmissionMode == TransmissionMode.STATIC_SLOTS) {
+ "🚀 静态独占槽位"
+ } else {
+ "📦 动态写码"
}
+
Text(
- text = modeText,
- style = MaterialTheme.typography.bodySmall,
+ text = "${remote.layoutStyle.name} · $modeText",
+ fontSize = 11.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+}
+
+@Composable
+private fun QuotaSummaryCard(quota: SlotQuotaSummary) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant,
+ ),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = "硬件槽位配额 (总计 100 槽位)",
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ )
+ Text(
+ text = "${quota.usedCount} / ${quota.totalCapacity} 已占用",
+ fontWeight = FontWeight.Bold,
+ color = if (quota.isStaticFull) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
+ fontSize = 13.sp,
+ )
+ }
+
+ LinearProgressIndicator(
+ progress = { quota.usedCount.toFloat() / quota.totalCapacity.toFloat() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(8.dp)
+ .clip(RoundedCornerShape(4.dp)),
+ )
+
+ Text(
+ text = "静态槽位占用: ${quota.assignedStaticSlots.size} 个 | 动态缓冲: Slot 100 (共享) | 剩余静态: ${quota.remainingStaticCount} 个",
+ fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@@ -336,15 +357,17 @@ private fun VirtualRemoteCard(
@Composable
private fun CreateVirtualRemoteDialog(
onDismiss: () -> Unit,
- onConfirm: (name: String, type: RemoteDeviceType, brandModel: String?, mode: TransmissionMode) -> Unit,
+ onConfirm: (name: String, type: RemoteDeviceType, brandModel: String?, transMode: TransmissionMode, layoutStyle: PanelLayoutStyle?) -> Unit,
) {
var name by remember { mutableStateOf("") }
var selectedType by remember { mutableStateOf(RemoteDeviceType.TV) }
var selectedBrand by remember { mutableStateOf(PresetCodeLibrary.availablePresets.first()) }
+ var selectedCustomLayout by remember { mutableStateOf(PanelLayoutStyle.TV_NAV) }
var transmissionMode by remember { mutableStateOf(TransmissionMode.STATIC_SLOTS) }
var typeDropdownExpanded by remember { mutableStateOf(false) }
var brandDropdownExpanded by remember { mutableStateOf(false) }
+ var layoutDropdownExpanded by remember { mutableStateOf(false) }
val filteredPresets = PresetCodeLibrary.availablePresets.filter { it.deviceType == selectedType }
@@ -393,6 +416,8 @@ private fun CreateVirtualRemoteDialog(
if (matched != null) selectedBrand = matched
if (type == RemoteDeviceType.AIR_CONDITIONER) {
transmissionMode = TransmissionMode.DYNAMIC_SCRATCH
+ } else if (type == RemoteDeviceType.CUSTOM) {
+ transmissionMode = TransmissionMode.STATIC_SLOTS
}
typeDropdownExpanded = false
},
@@ -405,13 +430,71 @@ private fun CreateVirtualRemoteDialog(
OutlinedTextField(
value = name,
onValueChange = { name = it },
- label = { Text("遥控器名称(如:客厅电视)") },
+ label = { Text("遥控器名称(如:客厅电视、我的遥控器)") },
singleLine = true,
modifier = Modifier.fillMaxWidth().testTag("create_remote_name_input"),
)
- // 3. 品牌 / 码库预设
- if (filteredPresets.isNotEmpty()) {
+ // 3. 自定义遥控器时:选择初始布局模板
+ if (selectedType == RemoteDeviceType.CUSTOM) {
+ ExposedDropdownMenuBox(
+ expanded = layoutDropdownExpanded,
+ onExpandedChange = { layoutDropdownExpanded = !layoutDropdownExpanded },
+ ) {
+ OutlinedTextField(
+ value = when (selectedCustomLayout) {
+ PanelLayoutStyle.TV_NAV -> "📺 电视导航布局 (内置方向盘/音量/频道)"
+ PanelLayoutStyle.MEDIA_PLAYER -> "🎵 媒体播放器布局 (播放/快进/切歌)"
+ PanelLayoutStyle.GRID -> "🎛️ 自由网格布局 (可自由增删按键)"
+ PanelLayoutStyle.AC_COMPACT -> "❄️ 空调布局"
+ },
+ onValueChange = {},
+ readOnly = true,
+ label = { Text("选择初始布局模板") },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = layoutDropdownExpanded) },
+ modifier = Modifier.fillMaxWidth().menuAnchor(),
+ )
+ ExposedDropdownMenu(
+ expanded = layoutDropdownExpanded,
+ onDismissRequest = { layoutDropdownExpanded = false },
+ ) {
+ DropdownMenuItem(
+ text = { Text("📺 电视导航布局 (内置方向盘/音量/频道)") },
+ onClick = {
+ selectedCustomLayout = PanelLayoutStyle.TV_NAV
+ layoutDropdownExpanded = false
+ },
+ )
+ DropdownMenuItem(
+ text = { Text("🎵 媒体播放器布局 (播放/快进/切歌)") },
+ onClick = {
+ selectedCustomLayout = PanelLayoutStyle.MEDIA_PLAYER
+ layoutDropdownExpanded = false
+ },
+ )
+ DropdownMenuItem(
+ text = { Text("🎛️ 自由网格布局 (可自由增删按键)") },
+ onClick = {
+ selectedCustomLayout = PanelLayoutStyle.GRID
+ layoutDropdownExpanded = false
+ },
+ )
+ }
+ }
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = "💡 自定义学习按键由硬件固化录制,将自动分配独占静态槽位 (1..99),创建后开启学习模式即可对准录码。",
+ fontSize = 12.sp,
+ modifier = Modifier.padding(10.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ } else if (filteredPresets.isNotEmpty()) {
+ // 码库预设设备:选择品牌型号
ExposedDropdownMenuBox(
expanded = brandDropdownExpanded,
onExpandedChange = { brandDropdownExpanded = !brandDropdownExpanded },
@@ -441,8 +524,8 @@ private fun CreateVirtualRemoteDialog(
}
}
- // 4. 传输模式选择(非空调设备可选)
- if (selectedType != RemoteDeviceType.AIR_CONDITIONER) {
+ // 4. 传输模式选择(仅对非空调、非纯自定义设备可选)
+ if (selectedType != RemoteDeviceType.AIR_CONDITIONER && selectedType != RemoteDeviceType.CUSTOM) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(stringResource(R.string.infrared_hub_trans_mode_label), fontWeight = FontWeight.Bold, fontSize = 13.sp)
@@ -474,13 +557,13 @@ private fun CreateVirtualRemoteDialog(
}
}
}
- } else {
+ } else if (selectedType == RemoteDeviceType.AIR_CONDITIONER) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)),
modifier = Modifier.fillMaxWidth(),
) {
Text(
- text = "❄️ 空调多维状态(温度、扫风、风速)自动采用 App 状态机 + 动态缓冲写码,杜绝状态踩踏并节省槽位。",
+ text = "❄️ 空调多维状态自动采用 App 状态机 + 动态缓冲写码,杜绝状态踩踏并节省槽位。",
fontSize = 12.sp,
modifier = Modifier.padding(10.dp),
color = MaterialTheme.colorScheme.onSecondaryContainer,
@@ -492,8 +575,9 @@ private fun CreateVirtualRemoteDialog(
confirmButton = {
Button(
onClick = {
- val finalName = if (name.isNotBlank()) name.trim() else selectedBrand.displayName
- onConfirm(finalName, selectedType, selectedBrand.id, transmissionMode)
+ val finalName = if (name.isNotBlank()) name.trim() else if (selectedType == RemoteDeviceType.CUSTOM) "自定义遥控器" else selectedBrand.displayName
+ val layout = if (selectedType == RemoteDeviceType.CUSTOM) selectedCustomLayout else null
+ onConfirm(finalName, selectedType, selectedBrand.id, transmissionMode, layout)
},
modifier = Modifier.testTag("create_remote_confirm_button"),
) {
diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModel.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModel.kt
index 579c57f..bfdad11 100644
--- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModel.kt
+++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModel.kt
@@ -126,6 +126,7 @@ class InfraredHubViewModel @Inject constructor(
type: RemoteDeviceType,
brandModel: String? = null,
transmissionMode: TransmissionMode,
+ layoutStyle: com.flagship.abox.manager.domain.infrared.PanelLayoutStyle? = null,
) {
val currentProfile = profileId ?: return
val currentDevice = state.value.savedDeviceName
@@ -137,6 +138,7 @@ class InfraredHubViewModel @Inject constructor(
type = type,
brandModel = brandModel,
transmissionMode = transmissionMode,
+ layoutStyle = layoutStyle,
)) {
is DomainResult.Success -> {
messageFlow.value = HubUiMessage.Info(UiText.DynamicString("已成功创建遥控器"))
diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelScreen.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelScreen.kt
index 76f130b..34f4dd6 100644
--- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelScreen.kt
+++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/panel/RemotePanelScreen.kt
@@ -87,11 +87,10 @@ fun RemotePanelRoute(
onToggleLearnMode = viewModel::toggleLearnMode,
onAddCustomButton = viewModel::addCustomButton,
onDeleteButton = viewModel::deleteButton,
- onSwitchLayout = viewModel::switchLayout,
onToggleAcPower = viewModel::toggleAcPower,
onAdjustAcTemp = viewModel::adjustAcTemperature,
onSetAcMode = viewModel::setAcMode,
- onSetAcFanSpeed = viewModel::setAcFanSpeed,
+ onSetFanSpeed = viewModel::setAcFanSpeed,
onToggleAcVerticalSwing = viewModel::toggleAcVerticalSwing,
onToggleAcHorizontalSwing = viewModel::toggleAcHorizontalSwing,
onToggleAcEco = viewModel::toggleAcEco,
@@ -109,11 +108,10 @@ internal fun RemotePanelScreen(
onToggleLearnMode: () -> Unit,
onAddCustomButton: (String) -> Unit,
onDeleteButton: (String) -> Unit,
- onSwitchLayout: (PanelLayoutStyle) -> Unit,
onToggleAcPower: () -> Unit,
onAdjustAcTemp: (Int) -> Unit,
onSetAcMode: (AcMode) -> Unit,
- onSetAcFanSpeed: (AcFanSpeed) -> Unit,
+ onSetFanSpeed: (AcFanSpeed) -> Unit,
onToggleAcVerticalSwing: () -> Unit,
onToggleAcHorizontalSwing: () -> Unit,
onToggleAcEco: () -> Unit,
@@ -123,7 +121,6 @@ internal fun RemotePanelScreen(
val context = LocalContext.current
var showAddKeyDialog by remember { mutableStateOf(false) }
- var showMenu by remember { mutableStateOf(false) }
LaunchedEffect(state.message) {
state.message?.let { msg ->
@@ -163,8 +160,14 @@ internal fun RemotePanelScreen(
fontSize = 18.sp,
)
state.remote?.let {
+ val layoutLabel = when (it.layoutStyle) {
+ PanelLayoutStyle.TV_NAV -> "电视导航布局"
+ PanelLayoutStyle.MEDIA_PLAYER -> "媒体播放布局"
+ PanelLayoutStyle.GRID -> "自由网格布局"
+ PanelLayoutStyle.AC_COMPACT -> "空调全状态布局"
+ }
Text(
- text = "布局: ${it.layoutStyle.name} | 模式: ${it.transmissionMode.name}",
+ text = layoutLabel,
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -177,53 +180,18 @@ internal fun RemotePanelScreen(
}
},
actions = {
- // 学习模式开关
- FilterChip(
- selected = state.isLearnMode,
- onClick = onToggleLearnMode,
- label = { Text(if (state.isLearnMode) "🔴 录制中" else "⚪ 学习模式") },
- colors = FilterChipDefaults.filterChipColors(
- selectedContainerColor = Color(0xFFFF5252),
- selectedLabelColor = Color.White,
- ),
- modifier = Modifier.testTag("learn_mode_toggle"),
- )
-
- // 更多选项菜单(添加按键、切换布局)
- Box {
- IconButton(onClick = { showMenu = true }, modifier = Modifier.testTag("panel_more_menu")) {
- Text("⋮", fontSize = 22.sp, fontWeight = FontWeight.Bold)
- }
- DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
- DropdownMenuItem(
- text = { Text("➕ 添加自定义按键") },
- onClick = {
- showMenu = false
- showAddKeyDialog = true
- },
- )
- DropdownMenuItem(
- text = { Text("📺 切换为电视导航布局") },
- onClick = {
- showMenu = false
- onSwitchLayout(PanelLayoutStyle.TV_NAV)
- },
- )
- DropdownMenuItem(
- text = { Text("🎛️ 切换为网格按键布局") },
- onClick = {
- showMenu = false
- onSwitchLayout(PanelLayoutStyle.GRID)
- },
- )
- DropdownMenuItem(
- text = { Text("🎵 切换为媒体播放布局") },
- onClick = {
- showMenu = false
- onSwitchLayout(PanelLayoutStyle.MEDIA_PLAYER)
- },
- )
- }
+ // 非空调面板显示学习模式开关
+ if (state.remote?.layoutStyle != PanelLayoutStyle.AC_COMPACT) {
+ FilterChip(
+ selected = state.isLearnMode,
+ onClick = onToggleLearnMode,
+ label = { Text(if (state.isLearnMode) "🔴 录制中" else "⚪ 学习模式") },
+ colors = FilterChipDefaults.filterChipColors(
+ selectedContainerColor = Color(0xFFFF5252),
+ selectedLabelColor = Color.White,
+ ),
+ modifier = Modifier.testTag("learn_mode_toggle"),
+ )
}
},
)
@@ -256,7 +224,7 @@ internal fun RemotePanelScreen(
) {
Text("🔴", fontSize = 16.sp)
Text(
- text = "学习录制模式已开启:点击下方任意按键,然后将实体遥控器对准转发器按下即可录码",
+ text = "学习模式已开启:点击任意按键,然后将物理实体遥控器对准转发器按下即可录码",
color = Color(0xFFC62828),
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
@@ -274,7 +242,7 @@ internal fun RemotePanelScreen(
onTogglePower = onToggleAcPower,
onAdjustTemp = onAdjustAcTemp,
onSetMode = onSetAcMode,
- onSetFanSpeed = onSetAcFanSpeed,
+ onSetFanSpeed = onSetFanSpeed,
onToggleVSwing = onToggleAcVerticalSwing,
onToggleHSwing = onToggleAcHorizontalSwing,
onToggleEco = onToggleAcEco,
@@ -287,7 +255,6 @@ internal fun RemotePanelScreen(
learningButtonId = state.learningButtonId,
sendingButtonId = state.sendingButtonId,
onButtonClick = onButtonClick,
- onDeleteButton = onDeleteButton,
)
}
PanelLayoutStyle.MEDIA_PLAYER -> {
@@ -479,8 +446,9 @@ private fun TvControlPanel(
learningButtonId: String?,
sendingButtonId: String?,
onButtonClick: (String) -> Unit,
- onDeleteButton: (String) -> Unit,
) {
+ val bindingMap = remember(bindings) { bindings.associateBy { it.buttonId } }
+
Column(
modifier = Modifier
.fillMaxSize()
@@ -494,9 +462,9 @@ private fun TvControlPanel(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
- InteractiveRemoteButton("TV_POWER", "⏻ 电源", Color(0xFFFF5252), isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
- InteractiveRemoteButton("TV_MUTE", "🔇 静音", MaterialTheme.colorScheme.secondary, isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
- InteractiveRemoteButton("TV_SOURCE", "📺 信号源", MaterialTheme.colorScheme.secondary, 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_SOURCE", "📺 信号源", MaterialTheme.colorScheme.secondary, bindingMap["TV_SOURCE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
}
Spacer(modifier = Modifier.height(8.dp))
@@ -556,9 +524,9 @@ private fun TvControlPanel(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
) {
- InteractiveRemoteButton("TV_BACK", "↩ 返回", MaterialTheme.colorScheme.outline, isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
- InteractiveRemoteButton("TV_HOME", "⌂ 主页", MaterialTheme.colorScheme.outline, isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
- InteractiveRemoteButton("TV_MENU", "☰ 菜单", MaterialTheme.colorScheme.outline, isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
+ InteractiveRemoteButton("TV_BACK", "↩ 返回", MaterialTheme.colorScheme.outline, bindingMap["TV_BACK"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
+ InteractiveRemoteButton("TV_HOME", "⌂ 主页", MaterialTheme.colorScheme.outline, bindingMap["TV_HOME"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
+ InteractiveRemoteButton("TV_MENU", "☰ 菜单", MaterialTheme.colorScheme.outline, bindingMap["TV_MENU"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
}
// 底部音量与频道调节柱
@@ -603,6 +571,8 @@ private fun MediaPlayerControlPanel(
sendingButtonId: String?,
onButtonClick: (String) -> Unit,
) {
+ val bindingMap = remember(bindings) { bindings.associateBy { it.buttonId } }
+
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -612,8 +582,8 @@ private fun MediaPlayerControlPanel(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
- InteractiveRemoteButton("MEDIA_POWER", "⏻ 电源", Color(0xFFFF5252), isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
- InteractiveRemoteButton("MEDIA_MUTE", "🔇 静音", MaterialTheme.colorScheme.secondary, 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)
}
Spacer(modifier = Modifier.height(16.dp))
@@ -677,7 +647,7 @@ private fun GenericGridControlPanel(
if (bindings.isEmpty()) {
Box(modifier = Modifier.fillMaxWidth().weight(1f), contentAlignment = Alignment.Center) {
- Text("暂无自定义按键,点击上方 ➕ 添加按键", color = MaterialTheme.colorScheme.onSurfaceVariant)
+ Text("暂无自定义按键,点击上方【➕ 添加按键】新建", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
} else {
LazyVerticalGrid(
@@ -713,13 +683,13 @@ private fun GridButtonCard(
Card(
modifier = Modifier
.fillMaxWidth()
- .height(72.dp)
+ .height(76.dp)
.clickable(onClick = onClick)
.testTag("generic_btn_${binding.buttonId}"),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
) {
Row(
- modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
+ modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
@@ -727,17 +697,21 @@ private fun GridButtonCard(
Text(binding.displayName, fontWeight = FontWeight.Bold, fontSize = 15.sp)
val statusText = when {
isLearning -> "🟡 正在录制信号..."
- binding.status == InfraredKeyStatus.READY -> "🟢 已录制"
- else -> "⚪ 未录制"
+ binding.status == InfraredKeyStatus.READY -> "🟢 已就绪 (Slot ${binding.assignedSlot ?: "动态"})"
+ else -> "⚪ 待录制 (点击录码)"
}
Text(statusText, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
if (isSending || isLearning) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
- } else if (isLearnMode) {
- IconButton(onClick = onDelete) {
- Text("✕", color = Color.Gray, fontSize = 14.sp)
+ } else {
+ // 始终提供删除操作按钮
+ IconButton(
+ onClick = onDelete,
+ modifier = Modifier.size(36.dp).testTag("delete_key_${binding.buttonId}"),
+ ) {
+ Text("🗑️", fontSize = 15.sp)
}
}
}
@@ -749,15 +723,23 @@ private fun InteractiveRemoteButton(
buttonId: String,
label: String,
containerColor: Color,
+ binding: RemoteKeyBinding?,
isLearnMode: Boolean,
learningButtonId: String?,
sendingButtonId: String?,
onButtonClick: (String) -> Unit,
) {
+ val isUnlearned = binding != null && binding.status != InfraredKeyStatus.READY
+
Button(
onClick = { onButtonClick(buttonId) },
colors = ButtonDefaults.buttonColors(
- containerColor = if (learningButtonId == buttonId) Color(0xFFFF9800) else containerColor,
+ containerColor = when {
+ learningButtonId == buttonId -> Color(0xFFFF9800)
+ isUnlearned -> MaterialTheme.colorScheme.surfaceVariant
+ else -> containerColor
+ },
+ contentColor = if (isUnlearned) MaterialTheme.colorScheme.onSurfaceVariant else Color.White,
),
modifier = Modifier.testTag("tv_btn_$buttonId"),
) {
@@ -792,11 +774,11 @@ private fun AddCustomKeyDialog(
title = { Text("添加自定义按键") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text("输入按键显示名称,创建后开启右上角【学习模式】即可录制实体信号:", fontSize = 13.sp)
+ Text("输入按键显示名称,创建后点击按键或开启【学习模式】即可录制实体按键:", fontSize = 13.sp)
OutlinedTextField(
value = name,
onValueChange = { name = it },
- label = { Text("按键名称(如:静音、音效切换)") },
+ label = { Text("按键名称(如:静音、低音增强)") },
singleLine = true,
modifier = Modifier.fillMaxWidth().testTag("add_key_name_input"),
)
diff --git a/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml b/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml
index c5be839..ef7d8bd 100644
--- a/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml
+++ b/android/feature/infrared/src/main/res/values-zh-rCN/strings.xml
@@ -88,6 +88,7 @@
按键已从本机删除
按键尚未就绪,无法发射
红外中枢
+ 我的遥控器
硬件槽位:%1$d / %2$d 已占用(剩余 %3$d)
静态占用:%1$d
动态缓冲:Slot %1$s
diff --git a/android/feature/infrared/src/main/res/values/strings.xml b/android/feature/infrared/src/main/res/values/strings.xml
index 88004a7..9535083 100644
--- a/android/feature/infrared/src/main/res/values/strings.xml
+++ b/android/feature/infrared/src/main/res/values/strings.xml
@@ -34,6 +34,7 @@
Back
Infrared Remote
Recording
+ My Remotes
Learn Mode
🔴 Learn mode active: tap any button to record
❄️ Air Conditioner
diff --git a/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModelTest.kt b/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModelTest.kt
index 3742cf3..2c5428a 100644
--- a/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModelTest.kt
+++ b/android/feature/infrared/src/test/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubViewModelTest.kt
@@ -133,4 +133,31 @@ class InfraredHubViewModelTest {
job.cancel()
}
+
+ @Test
+ fun createCustomRemoteWithTvLayoutAssignsLayoutAndStaticSlots() = testScope.runTest {
+ profileStore.saveProfile(providerProfile)
+ deviceRepo.setDeviceName(profileId, device)
+
+ val vm = createViewModel()
+ val job = launch { vm.state.collect {} }
+ advanceUntilIdle()
+
+ vm.createRemote(
+ name = "Custom TV Remote",
+ type = RemoteDeviceType.CUSTOM,
+ brandModel = null,
+ transmissionMode = TransmissionMode.STATIC_SLOTS,
+ layoutStyle = com.flagship.abox.manager.domain.infrared.PanelLayoutStyle.TV_NAV,
+ )
+ advanceUntilIdle()
+
+ assertEquals(1, vm.state.value.remotes.size)
+ val created = vm.state.value.remotes.first()
+ assertEquals("Custom TV Remote", created.displayName)
+ assertEquals(com.flagship.abox.manager.domain.infrared.PanelLayoutStyle.TV_NAV, created.layoutStyle)
+ assertEquals(TransmissionMode.STATIC_SLOTS, created.transmissionMode)
+
+ job.cancel()
+ }
}
diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/CreateVirtualRemote.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/CreateVirtualRemote.kt
index b771bea..cc2f747 100644
--- a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/CreateVirtualRemote.kt
+++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/CreateVirtualRemote.kt
@@ -19,7 +19,8 @@ class CreateVirtualRemote(
displayName: String,
type: RemoteDeviceType,
brandModel: String? = null,
- transmissionMode: TransmissionMode,
+ transmissionMode: TransmissionMode = if (type == RemoteDeviceType.CUSTOM) TransmissionMode.STATIC_SLOTS else TransmissionMode.DYNAMIC_SCRATCH,
+ layoutStyle: PanelLayoutStyle? = null,
): DomainResult {
val trimmedName = displayName.trim()
if (trimmedName.isBlank()) {
@@ -28,12 +29,14 @@ class CreateVirtualRemote(
)
}
- val templates = PresetCodeLibrary.getTemplatesForType(type, brandModel)
- val quotaResult = repository.getKeyBindings(VirtualRemoteId("dummy")) // or compute from quota
- // Obtain slot quota
- // We calculate needed static slots:
- val neededStaticSlots = if (transmissionMode == TransmissionMode.STATIC_SLOTS) templates.size else 0
+ val resolvedLayout = layoutStyle ?: VirtualRemote.defaultLayoutFor(type)
+ val resolvedTransMode = if (type == RemoteDeviceType.CUSTOM) {
+ TransmissionMode.STATIC_SLOTS
+ } else {
+ transmissionMode
+ }
+ val templates = PresetCodeLibrary.getTemplatesForType(type, brandModel, resolvedLayout)
val remoteId = VirtualRemoteId(UUID.randomUUID().toString().replace("-", "").take(16))
val now = clock.now()
@@ -46,7 +49,8 @@ class CreateVirtualRemote(
displayName = trimmedName,
type = type,
brandModel = brandModel,
- transmissionMode = transmissionMode,
+ layoutStyle = resolvedLayout,
+ transmissionMode = resolvedTransMode,
dynamicScratchSlot = VirtualRemote.DEFAULT_DYNAMIC_SCRATCH_SLOT,
acState = initialAcState,
createdAtEpochMillis = now,
@@ -54,9 +58,7 @@ class CreateVirtualRemote(
)
// Build key bindings
- val bindings = if (transmissionMode == TransmissionMode.STATIC_SLOTS) {
- // Find currently used slots by querying quota
- // For now, assign slots incrementally or check
+ val bindings = if (resolvedTransMode == TransmissionMode.STATIC_SLOTS) {
templates.mapIndexed { index, template ->
RemoteKeyBinding(
id = UUID.randomUUID().toString(),
@@ -64,7 +66,7 @@ class CreateVirtualRemote(
profileId = profileId,
buttonId = template.buttonId,
displayName = template.displayName,
- assignedSlot = (index + 1).toString(), // Will be allocated based on allocator
+ assignedSlot = (index + 1).toString(),
rawHexCode = template.defaultHexCode,
status = if (template.defaultHexCode != null) InfraredKeyStatus.READY else InfraredKeyStatus.FAILED,
)
@@ -89,7 +91,7 @@ class CreateVirtualRemote(
}
// If static and has default hex codes, download to hardware slots
- if (transmissionMode == TransmissionMode.STATIC_SLOTS) {
+ if (resolvedTransMode == TransmissionMode.STATIC_SLOTS) {
val codesToDownload = bindings.mapNotNull { b ->
if (b.assignedSlot != null && b.rawHexCode != null) {
InfraredCode(deviceName, b.assignedSlot, b.rawHexCode)
diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/PresetCodeLibrary.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/PresetCodeLibrary.kt
index 8785d74..7a521c7 100644
--- a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/PresetCodeLibrary.kt
+++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/PresetCodeLibrary.kt
@@ -26,7 +26,7 @@ object PresetCodeLibrary {
PresetBrandOption("generic_fan", "通用风扇 (Generic Fan)", RemoteDeviceType.FAN),
// 自定义
- PresetBrandOption("custom_blank", "自定义学习遥控器 (Custom)", RemoteDeviceType.CUSTOM),
+ PresetBrandOption("custom_blank", "自定义遥控器 (Custom)", RemoteDeviceType.CUSTOM),
)
data class PresetButtonTemplate(
@@ -35,24 +35,12 @@ object PresetCodeLibrary {
val defaultHexCode: String? = null,
)
- fun getTemplatesForType(type: RemoteDeviceType, presetId: String? = null): List = when (type) {
- RemoteDeviceType.TV -> listOf(
- PresetButtonTemplate("TV_POWER", "电源", "0x00FF02FD"),
- PresetButtonTemplate("TV_MUTE", "静音", "0x00FF04FB"),
- PresetButtonTemplate("TV_SOURCE", "信号源", "0x00FF06F9"),
- PresetButtonTemplate("TV_NAV_UP", "上", "0x00FF08F7"),
- PresetButtonTemplate("TV_NAV_DOWN", "下", "0x00FF0AF5"),
- PresetButtonTemplate("TV_NAV_LEFT", "左", "0x00FF0CF3"),
- PresetButtonTemplate("TV_NAV_RIGHT", "右", "0x00FF0EF1"),
- PresetButtonTemplate("TV_OK", "确定", "0x00FF10EF"),
- PresetButtonTemplate("TV_HOME", "主页", "0x00FF12ED"),
- PresetButtonTemplate("TV_BACK", "返回", "0x00FF14EB"),
- PresetButtonTemplate("TV_MENU", "菜单", "0x00FF16E9"),
- PresetButtonTemplate("TV_VOL_UP", "音量 +", "0x00FF18E7"),
- PresetButtonTemplate("TV_VOL_DOWN", "音量 -", "0x00FF1AE5"),
- PresetButtonTemplate("TV_CH_UP", "频道 +", "0x00FF1CE3"),
- PresetButtonTemplate("TV_CH_DOWN", "频道 -", "0x00FF1EE1"),
- )
+ fun getTemplatesForType(
+ type: RemoteDeviceType,
+ presetId: String? = null,
+ layoutStyle: PanelLayoutStyle? = null,
+ ): List = when (type) {
+ RemoteDeviceType.TV -> getTvTemplates(hasHexCode = true)
RemoteDeviceType.AIR_CONDITIONER -> listOf(
PresetButtonTemplate("AC_POWER", "电源"),
PresetButtonTemplate("AC_TEMP_UP", "升温"),
@@ -70,12 +58,44 @@ object PresetCodeLibrary {
PresetButtonTemplate("FAN_TIMER", "定时", "0x01FE08F7"),
PresetButtonTemplate("FAN_MODE", "模式", "0x01FE0AF5"),
)
- RemoteDeviceType.CUSTOM -> listOf(
- PresetButtonTemplate("CUSTOM_POWER", "电源"),
- PresetButtonTemplate("CUSTOM_KEY_1", "按键 1"),
- PresetButtonTemplate("CUSTOM_KEY_2", "按键 2"),
- PresetButtonTemplate("CUSTOM_KEY_3", "按键 3"),
- PresetButtonTemplate("CUSTOM_KEY_4", "按键 4"),
- )
+ RemoteDeviceType.CUSTOM -> when (layoutStyle) {
+ PanelLayoutStyle.TV_NAV -> getTvTemplates(hasHexCode = false)
+ PanelLayoutStyle.MEDIA_PLAYER -> getMediaPlayerTemplates(hasHexCode = false)
+ PanelLayoutStyle.AC_COMPACT -> emptyList()
+ PanelLayoutStyle.GRID, null -> listOf(
+ PresetButtonTemplate("CUSTOM_POWER", "电源"),
+ PresetButtonTemplate("CUSTOM_KEY_1", "按键 1"),
+ PresetButtonTemplate("CUSTOM_KEY_2", "按键 2"),
+ PresetButtonTemplate("CUSTOM_KEY_3", "按键 3"),
+ )
+ }
}
+
+ private fun getTvTemplates(hasHexCode: Boolean): List = listOf(
+ PresetButtonTemplate("TV_POWER", "电源", if (hasHexCode) "0x00FF02FD" else null),
+ PresetButtonTemplate("TV_MUTE", "静音", if (hasHexCode) "0x00FF04FB" else null),
+ PresetButtonTemplate("TV_SOURCE", "信号源", if (hasHexCode) "0x00FF06F9" else null),
+ PresetButtonTemplate("TV_NAV_UP", "上", if (hasHexCode) "0x00FF08F7" else null),
+ PresetButtonTemplate("TV_NAV_DOWN", "下", if (hasHexCode) "0x00FF0AF5" else null),
+ PresetButtonTemplate("TV_NAV_LEFT", "左", if (hasHexCode) "0x00FF0CF3" else null),
+ PresetButtonTemplate("TV_NAV_RIGHT", "右", if (hasHexCode) "0x00FF0EF1" else null),
+ PresetButtonTemplate("TV_OK", "确定", if (hasHexCode) "0x00FF10EF" else null),
+ PresetButtonTemplate("TV_HOME", "主页", if (hasHexCode) "0x00FF12ED" else null),
+ PresetButtonTemplate("TV_BACK", "返回", if (hasHexCode) "0x00FF14EB" else null),
+ PresetButtonTemplate("TV_MENU", "菜单", if (hasHexCode) "0x00FF16E9" else null),
+ PresetButtonTemplate("TV_VOL_UP", "音量 +", if (hasHexCode) "0x00FF18E7" else null),
+ PresetButtonTemplate("TV_VOL_DOWN", "音量 -", if (hasHexCode) "0x00FF1AE5" else null),
+ PresetButtonTemplate("TV_CH_UP", "频道 +", if (hasHexCode) "0x00FF1CE3" else null),
+ PresetButtonTemplate("TV_CH_DOWN", "频道 -", if (hasHexCode) "0x00FF1EE1" else null),
+ )
+
+ private fun getMediaPlayerTemplates(hasHexCode: Boolean): List = listOf(
+ PresetButtonTemplate("MEDIA_POWER", "电源", if (hasHexCode) "0x02FD02FD" else null),
+ PresetButtonTemplate("MEDIA_MUTE", "静音", if (hasHexCode) "0x02FD04FB" else null),
+ PresetButtonTemplate("MEDIA_PLAY_PAUSE", "播放/暂停", if (hasHexCode) "0x02FD06F9" else null),
+ PresetButtonTemplate("MEDIA_PREV", "上一曲", if (hasHexCode) "0x02FD08F7" else null),
+ PresetButtonTemplate("MEDIA_NEXT", "下一曲", if (hasHexCode) "0x02FD0AF5" else null),
+ PresetButtonTemplate("MEDIA_REWIND", "快退", if (hasHexCode) "0x02FD0CF3" else null),
+ PresetButtonTemplate("MEDIA_FF", "快进", if (hasHexCode) "0x02FD0EF1" else null),
+ )
}