feat(infrared): choose layout template at creation time and improve grid key management

This commit is contained in:
2026-08-25 14:34:47 +08:00
parent 849b7e4aea
commit ec51ae1db7
8 changed files with 321 additions and 202 deletions
@@ -1,5 +1,6 @@
package com.flagship.abox.manager.infrared.hub package com.flagship.abox.manager.infrared.hub
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.FilterChip
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -31,6 +36,7 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
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
@@ -42,6 +48,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -51,10 +59,11 @@ 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.domain.ProviderType import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.domain.infrared.PresetBrandOption import com.flagship.abox.manager.domain.infrared.PanelLayoutStyle
import com.flagship.abox.manager.domain.infrared.PresetCodeLibrary import com.flagship.abox.manager.domain.infrared.PresetCodeLibrary
import com.flagship.abox.manager.domain.infrared.RemoteDeviceType 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.TransmissionMode
import com.flagship.abox.manager.domain.infrared.VirtualRemote import com.flagship.abox.manager.domain.infrared.VirtualRemote
import com.flagship.abox.manager.domain.infrared.VirtualRemoteId import com.flagship.abox.manager.domain.infrared.VirtualRemoteId
@@ -74,12 +83,11 @@ fun InfraredHubRoute(
InfraredHubScreen( InfraredHubScreen(
state = state, state = state,
onBack = onBack, onBack = onBack,
onNavigateToSettings = onOpenSettings,
onOpenRemote = onOpenRemote, onOpenRemote = onOpenRemote,
onOpenSettings = onOpenSettings,
onSessionInvalid = onSessionInvalid,
onCreateRemote = viewModel::createRemote,
onDeleteRemote = viewModel::deleteRemote, onDeleteRemote = viewModel::deleteRemote,
onSaveDeviceName = viewModel::saveDeviceName, onCreateRemote = viewModel::createRemote,
onSessionInvalid = onSessionInvalid,
onMessageShown = viewModel::clearMessage, onMessageShown = viewModel::clearMessage,
) )
} }
@@ -89,12 +97,11 @@ fun InfraredHubRoute(
internal fun InfraredHubScreen( internal fun InfraredHubScreen(
state: InfraredHubUiState, state: InfraredHubUiState,
onBack: () -> Unit, onBack: () -> Unit,
onNavigateToSettings: () -> Unit,
onOpenRemote: (String) -> Unit, onOpenRemote: (String) -> Unit,
onOpenSettings: () -> Unit,
onSessionInvalid: () -> Unit,
onCreateRemote: (String, RemoteDeviceType, String?, TransmissionMode) -> Unit,
onDeleteRemote: (VirtualRemoteId) -> Unit, onDeleteRemote: (VirtualRemoteId) -> Unit,
onSaveDeviceName: (String) -> Unit, onCreateRemote: (String, RemoteDeviceType, String?, TransmissionMode, PanelLayoutStyle?) -> Unit,
onSessionInvalid: () -> Unit,
onMessageShown: () -> Unit, onMessageShown: () -> Unit,
) { ) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
@@ -131,7 +138,21 @@ internal fun InfraredHubScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( 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 = { navigationIcon = {
TextButton(onClick = onBack) { TextButton(onClick = onBack) {
Text(stringResource(R.string.infrared_back)) Text(stringResource(R.string.infrared_back))
@@ -139,7 +160,7 @@ internal fun InfraredHubScreen(
}, },
actions = { actions = {
IconButton( IconButton(
onClick = onOpenSettings, onClick = onNavigateToSettings,
modifier = Modifier.testTag("hub_settings_button"), modifier = Modifier.testTag("hub_settings_button"),
) { ) {
Text("⚙️", fontSize = 20.sp) Text("⚙️", fontSize = 20.sp)
@@ -160,71 +181,20 @@ internal fun InfraredHubScreen(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
// 1. 顶部环境与 100 槽位看板卡片 // 1. 硬件槽位配额看板 (100 槽位预算)
Card(modifier = Modifier.fillMaxWidth()) { QuotaSummaryCard(quota = state.slotQuota)
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,
)
Text( // 2. 遥控器列表标题与新建按钮
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. 虚拟遥控器列表标题栏
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(R.string.infrared_hub_remotes_title), text = stringResource(R.string.infrared_hub_my_remotes),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
) )
Button( Button(
onClick = { showCreateDialog = true }, onClick = { showCreateDialog = true },
modifier = Modifier.testTag("hub_add_remote_button"), modifier = Modifier.testTag("hub_add_remote_button"),
@@ -268,8 +238,8 @@ internal fun InfraredHubScreen(
if (showCreateDialog) { if (showCreateDialog) {
CreateVirtualRemoteDialog( CreateVirtualRemoteDialog(
onDismiss = { showCreateDialog = false }, onDismiss = { showCreateDialog = false },
onConfirm = { name, type, brandModel, transMode -> onConfirm = { name, type, brandModel, transMode, layoutStyle ->
onCreateRemote(name, type, brandModel, transMode) onCreateRemote(name, type, brandModel, transMode, layoutStyle)
showCreateDialog = false showCreateDialog = false
}, },
) )
@@ -318,14 +288,65 @@ private fun VirtualRemoteCard(
) )
val modeText = if (remote.type == RemoteDeviceType.AIR_CONDITIONER) { val modeText = if (remote.type == RemoteDeviceType.AIR_CONDITIONER) {
stringResource(R.string.infrared_hub_mode_dynamic, "100") "❄️ 状态机动态写码"
} else when (remote.transmissionMode) { } else if (remote.transmissionMode == TransmissionMode.STATIC_SLOTS) {
TransmissionMode.STATIC_SLOTS -> stringResource(R.string.infrared_hub_mode_static, "15") "🚀 静态独占槽位"
TransmissionMode.DYNAMIC_SCRATCH -> stringResource(R.string.infrared_hub_mode_dynamic, "100") } else {
"📦 动态写码"
} }
Text( Text(
text = modeText, text = "${remote.layoutStyle.name} · $modeText",
style = MaterialTheme.typography.bodySmall, 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, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
@@ -336,15 +357,17 @@ private fun VirtualRemoteCard(
@Composable @Composable
private fun CreateVirtualRemoteDialog( private fun CreateVirtualRemoteDialog(
onDismiss: () -> Unit, 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 name by remember { mutableStateOf("") }
var selectedType by remember { mutableStateOf(RemoteDeviceType.TV) } var selectedType by remember { mutableStateOf(RemoteDeviceType.TV) }
var selectedBrand by remember { mutableStateOf(PresetCodeLibrary.availablePresets.first()) } 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 transmissionMode by remember { mutableStateOf(TransmissionMode.STATIC_SLOTS) }
var typeDropdownExpanded by remember { mutableStateOf(false) } var typeDropdownExpanded by remember { mutableStateOf(false) }
var brandDropdownExpanded by remember { mutableStateOf(false) } var brandDropdownExpanded by remember { mutableStateOf(false) }
var layoutDropdownExpanded by remember { mutableStateOf(false) }
val filteredPresets = PresetCodeLibrary.availablePresets.filter { it.deviceType == selectedType } val filteredPresets = PresetCodeLibrary.availablePresets.filter { it.deviceType == selectedType }
@@ -393,6 +416,8 @@ private fun CreateVirtualRemoteDialog(
if (matched != null) selectedBrand = matched if (matched != null) selectedBrand = matched
if (type == RemoteDeviceType.AIR_CONDITIONER) { if (type == RemoteDeviceType.AIR_CONDITIONER) {
transmissionMode = TransmissionMode.DYNAMIC_SCRATCH transmissionMode = TransmissionMode.DYNAMIC_SCRATCH
} else if (type == RemoteDeviceType.CUSTOM) {
transmissionMode = TransmissionMode.STATIC_SLOTS
} }
typeDropdownExpanded = false typeDropdownExpanded = false
}, },
@@ -405,13 +430,71 @@ 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"),
) )
// 3. 品牌 / 码库预设 // 3. 自定义遥控器时:选择初始布局模板
if (filteredPresets.isNotEmpty()) { 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( ExposedDropdownMenuBox(
expanded = brandDropdownExpanded, expanded = brandDropdownExpanded,
onExpandedChange = { brandDropdownExpanded = !brandDropdownExpanded }, onExpandedChange = { brandDropdownExpanded = !brandDropdownExpanded },
@@ -441,8 +524,8 @@ private fun CreateVirtualRemoteDialog(
} }
} }
// 4. 传输模式选择(非空调设备可选) // 4. 传输模式选择(仅对非空调、非纯自定义设备可选)
if (selectedType != RemoteDeviceType.AIR_CONDITIONER) { if (selectedType != RemoteDeviceType.AIR_CONDITIONER && selectedType != RemoteDeviceType.CUSTOM) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(stringResource(R.string.infrared_hub_trans_mode_label), fontWeight = FontWeight.Bold, fontSize = 13.sp) 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( Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Text( Text(
text = "❄️ 空调多维状态(温度、扫风、风速)自动采用 App 状态机 + 动态缓冲写码,杜绝状态踩踏并节省槽位。", text = "❄️ 空调多维状态自动采用 App 状态机 + 动态缓冲写码,杜绝状态踩踏并节省槽位。",
fontSize = 12.sp, fontSize = 12.sp,
modifier = Modifier.padding(10.dp), modifier = Modifier.padding(10.dp),
color = MaterialTheme.colorScheme.onSecondaryContainer, color = MaterialTheme.colorScheme.onSecondaryContainer,
@@ -492,8 +575,9 @@ private fun CreateVirtualRemoteDialog(
confirmButton = { confirmButton = {
Button( Button(
onClick = { onClick = {
val finalName = if (name.isNotBlank()) name.trim() else selectedBrand.displayName val finalName = if (name.isNotBlank()) name.trim() else if (selectedType == RemoteDeviceType.CUSTOM) "自定义遥控器" else selectedBrand.displayName
onConfirm(finalName, selectedType, selectedBrand.id, transmissionMode) val layout = if (selectedType == RemoteDeviceType.CUSTOM) selectedCustomLayout else null
onConfirm(finalName, selectedType, selectedBrand.id, transmissionMode, layout)
}, },
modifier = Modifier.testTag("create_remote_confirm_button"), modifier = Modifier.testTag("create_remote_confirm_button"),
) { ) {
@@ -126,6 +126,7 @@ class InfraredHubViewModel @Inject constructor(
type: RemoteDeviceType, type: RemoteDeviceType,
brandModel: String? = null, brandModel: String? = null,
transmissionMode: TransmissionMode, transmissionMode: TransmissionMode,
layoutStyle: com.flagship.abox.manager.domain.infrared.PanelLayoutStyle? = null,
) { ) {
val currentProfile = profileId ?: return val currentProfile = profileId ?: return
val currentDevice = state.value.savedDeviceName val currentDevice = state.value.savedDeviceName
@@ -137,6 +138,7 @@ class InfraredHubViewModel @Inject constructor(
type = type, type = type,
brandModel = brandModel, brandModel = brandModel,
transmissionMode = transmissionMode, transmissionMode = transmissionMode,
layoutStyle = layoutStyle,
)) { )) {
is DomainResult.Success -> { is DomainResult.Success -> {
messageFlow.value = HubUiMessage.Info(UiText.DynamicString("已成功创建遥控器")) messageFlow.value = HubUiMessage.Info(UiText.DynamicString("已成功创建遥控器"))
@@ -87,11 +87,10 @@ fun RemotePanelRoute(
onToggleLearnMode = viewModel::toggleLearnMode, onToggleLearnMode = viewModel::toggleLearnMode,
onAddCustomButton = viewModel::addCustomButton, onAddCustomButton = viewModel::addCustomButton,
onDeleteButton = viewModel::deleteButton, onDeleteButton = viewModel::deleteButton,
onSwitchLayout = viewModel::switchLayout,
onToggleAcPower = viewModel::toggleAcPower, onToggleAcPower = viewModel::toggleAcPower,
onAdjustAcTemp = viewModel::adjustAcTemperature, onAdjustAcTemp = viewModel::adjustAcTemperature,
onSetAcMode = viewModel::setAcMode, onSetAcMode = viewModel::setAcMode,
onSetAcFanSpeed = viewModel::setAcFanSpeed, onSetFanSpeed = viewModel::setAcFanSpeed,
onToggleAcVerticalSwing = viewModel::toggleAcVerticalSwing, onToggleAcVerticalSwing = viewModel::toggleAcVerticalSwing,
onToggleAcHorizontalSwing = viewModel::toggleAcHorizontalSwing, onToggleAcHorizontalSwing = viewModel::toggleAcHorizontalSwing,
onToggleAcEco = viewModel::toggleAcEco, onToggleAcEco = viewModel::toggleAcEco,
@@ -109,11 +108,10 @@ internal fun RemotePanelScreen(
onToggleLearnMode: () -> Unit, onToggleLearnMode: () -> Unit,
onAddCustomButton: (String) -> Unit, onAddCustomButton: (String) -> Unit,
onDeleteButton: (String) -> Unit, onDeleteButton: (String) -> Unit,
onSwitchLayout: (PanelLayoutStyle) -> Unit,
onToggleAcPower: () -> Unit, onToggleAcPower: () -> Unit,
onAdjustAcTemp: (Int) -> Unit, onAdjustAcTemp: (Int) -> Unit,
onSetAcMode: (AcMode) -> Unit, onSetAcMode: (AcMode) -> Unit,
onSetAcFanSpeed: (AcFanSpeed) -> Unit, onSetFanSpeed: (AcFanSpeed) -> Unit,
onToggleAcVerticalSwing: () -> Unit, onToggleAcVerticalSwing: () -> Unit,
onToggleAcHorizontalSwing: () -> Unit, onToggleAcHorizontalSwing: () -> Unit,
onToggleAcEco: () -> Unit, onToggleAcEco: () -> Unit,
@@ -123,7 +121,6 @@ internal fun RemotePanelScreen(
val context = LocalContext.current val context = LocalContext.current
var showAddKeyDialog by remember { mutableStateOf(false) } var showAddKeyDialog by remember { mutableStateOf(false) }
var showMenu by remember { mutableStateOf(false) }
LaunchedEffect(state.message) { LaunchedEffect(state.message) {
state.message?.let { msg -> state.message?.let { msg ->
@@ -163,8 +160,14 @@ internal fun RemotePanelScreen(
fontSize = 18.sp, fontSize = 18.sp,
) )
state.remote?.let { state.remote?.let {
val layoutLabel = when (it.layoutStyle) {
PanelLayoutStyle.TV_NAV -> "电视导航布局"
PanelLayoutStyle.MEDIA_PLAYER -> "媒体播放布局"
PanelLayoutStyle.GRID -> "自由网格布局"
PanelLayoutStyle.AC_COMPACT -> "空调全状态布局"
}
Text( Text(
text = "布局: ${it.layoutStyle.name} | 模式: ${it.transmissionMode.name}", text = layoutLabel,
fontSize = 11.sp, fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -177,7 +180,8 @@ internal fun RemotePanelScreen(
} }
}, },
actions = { actions = {
// 学习模式开关 // 非空调面板显示学习模式开关
if (state.remote?.layoutStyle != PanelLayoutStyle.AC_COMPACT) {
FilterChip( FilterChip(
selected = state.isLearnMode, selected = state.isLearnMode,
onClick = onToggleLearnMode, onClick = onToggleLearnMode,
@@ -188,42 +192,6 @@ internal fun RemotePanelScreen(
), ),
modifier = Modifier.testTag("learn_mode_toggle"), 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)
},
)
}
} }
}, },
) )
@@ -256,7 +224,7 @@ internal fun RemotePanelScreen(
) { ) {
Text("🔴", fontSize = 16.sp) Text("🔴", fontSize = 16.sp)
Text( Text(
text = "学习录制模式已开启:点击下方任意按键,然后将实体遥控器对准转发器按下即可录码", text = "学习模式已开启:点击任意按键,然后将物理实体遥控器对准转发器按下即可录码",
color = Color(0xFFC62828), color = Color(0xFFC62828),
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
@@ -274,7 +242,7 @@ internal fun RemotePanelScreen(
onTogglePower = onToggleAcPower, onTogglePower = onToggleAcPower,
onAdjustTemp = onAdjustAcTemp, onAdjustTemp = onAdjustAcTemp,
onSetMode = onSetAcMode, onSetMode = onSetAcMode,
onSetFanSpeed = onSetAcFanSpeed, onSetFanSpeed = onSetFanSpeed,
onToggleVSwing = onToggleAcVerticalSwing, onToggleVSwing = onToggleAcVerticalSwing,
onToggleHSwing = onToggleAcHorizontalSwing, onToggleHSwing = onToggleAcHorizontalSwing,
onToggleEco = onToggleAcEco, onToggleEco = onToggleAcEco,
@@ -287,7 +255,6 @@ internal fun RemotePanelScreen(
learningButtonId = state.learningButtonId, learningButtonId = state.learningButtonId,
sendingButtonId = state.sendingButtonId, sendingButtonId = state.sendingButtonId,
onButtonClick = onButtonClick, onButtonClick = onButtonClick,
onDeleteButton = onDeleteButton,
) )
} }
PanelLayoutStyle.MEDIA_PLAYER -> { PanelLayoutStyle.MEDIA_PLAYER -> {
@@ -479,8 +446,9 @@ private fun TvControlPanel(
learningButtonId: String?, learningButtonId: String?,
sendingButtonId: String?, sendingButtonId: String?,
onButtonClick: (String) -> Unit, onButtonClick: (String) -> Unit,
onDeleteButton: (String) -> Unit,
) { ) {
val bindingMap = remember(bindings) { bindings.associateBy { it.buttonId } }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -494,9 +462,9 @@ private fun TvControlPanel(
horizontalArrangement = Arrangement.SpaceAround, horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
InteractiveRemoteButton("TV_POWER", "⏻ 电源", Color(0xFFFF5252), isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("TV_POWER", "⏻ 电源", Color(0xFFFF5252), bindingMap["TV_POWER"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
InteractiveRemoteButton("TV_MUTE", "🔇 静音", MaterialTheme.colorScheme.secondary, isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("TV_MUTE", "🔇 静音", MaterialTheme.colorScheme.secondary, bindingMap["TV_MUTE"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
InteractiveRemoteButton("TV_SOURCE", "📺 信号源", MaterialTheme.colorScheme.secondary, 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))
@@ -556,9 +524,9 @@ private fun TvControlPanel(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround, horizontalArrangement = Arrangement.SpaceAround,
) { ) {
InteractiveRemoteButton("TV_BACK", "↩ 返回", 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, isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("TV_HOME", "⌂ 主页", MaterialTheme.colorScheme.outline, bindingMap["TV_HOME"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
InteractiveRemoteButton("TV_MENU", "☰ 菜单", MaterialTheme.colorScheme.outline, 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?, sendingButtonId: String?,
onButtonClick: (String) -> Unit, onButtonClick: (String) -> Unit,
) { ) {
val bindingMap = remember(bindings) { bindings.associateBy { it.buttonId } }
Column( Column(
modifier = Modifier.fillMaxSize().padding(24.dp), modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -612,8 +582,8 @@ private fun MediaPlayerControlPanel(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
InteractiveRemoteButton("MEDIA_POWER", "⏻ 电源", Color(0xFFFF5252), isLearnMode, learningButtonId, sendingButtonId, onButtonClick) InteractiveRemoteButton("MEDIA_POWER", "⏻ 电源", Color(0xFFFF5252), bindingMap["MEDIA_POWER"], isLearnMode, learningButtonId, sendingButtonId, onButtonClick)
InteractiveRemoteButton("MEDIA_MUTE", "🔇 静音", MaterialTheme.colorScheme.secondary, 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))
@@ -677,7 +647,7 @@ private fun GenericGridControlPanel(
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(
@@ -713,13 +683,13 @@ private fun GridButtonCard(
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(72.dp) .height(76.dp)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.testTag("generic_btn_${binding.buttonId}"), .testTag("generic_btn_${binding.buttonId}"),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp), modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@@ -727,17 +697,21 @@ private fun GridButtonCard(
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 -> "🟢 已录制" binding.status == InfraredKeyStatus.READY -> "🟢 已就绪 (Slot ${binding.assignedSlot ?: "动态"})"
else -> "⚪ 未录制" else -> "⚪ 待录制 (点击录码)"
} }
Text(statusText, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(statusText, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
if (isSending || isLearning) { if (isSending || isLearning) {
CircularProgressIndicator(modifier = Modifier.size(20.dp)) CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else if (isLearnMode) { } else {
IconButton(onClick = onDelete) { // 始终提供删除操作按钮
Text("✕", color = Color.Gray, fontSize = 14.sp) 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, buttonId: String,
label: String, label: String,
containerColor: Color, containerColor: Color,
binding: RemoteKeyBinding?,
isLearnMode: Boolean, isLearnMode: Boolean,
learningButtonId: String?, learningButtonId: String?,
sendingButtonId: String?, sendingButtonId: String?,
onButtonClick: (String) -> Unit, onButtonClick: (String) -> Unit,
) { ) {
val isUnlearned = binding != null && binding.status != InfraredKeyStatus.READY
Button( Button(
onClick = { onButtonClick(buttonId) }, onClick = { onButtonClick(buttonId) },
colors = ButtonDefaults.buttonColors( 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"), modifier = Modifier.testTag("tv_btn_$buttonId"),
) { ) {
@@ -792,11 +774,11 @@ private fun AddCustomKeyDialog(
title = { Text("添加自定义按键") }, title = { Text("添加自定义按键") },
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("输入按键显示名称,创建后开启右上角【学习模式】即可录制实体信号:", fontSize = 13.sp) Text("输入按键显示名称,创建后点击按键或开启【学习模式】即可录制实体按键:", fontSize = 13.sp)
OutlinedTextField( OutlinedTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("按键名称(如:静音、音效切换)") }, label = { Text("按键名称(如:静音、低音增强)") },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth().testTag("add_key_name_input"), modifier = Modifier.fillMaxWidth().testTag("add_key_name_input"),
) )
@@ -88,6 +88,7 @@
<string name="infrared_msg_key_deleted">按键已从本机删除</string> <string name="infrared_msg_key_deleted">按键已从本机删除</string>
<string name="infrared_msg_key_not_ready">按键尚未就绪,无法发射</string> <string name="infrared_msg_key_not_ready">按键尚未就绪,无法发射</string>
<string name="infrared_hub_title">红外中枢</string> <string name="infrared_hub_title">红外中枢</string>
<string name="infrared_hub_my_remotes">我的遥控器</string>
<string name="infrared_hub_slot_quota">硬件槽位:%1$d / %2$d 已占用(剩余 %3$d)</string> <string name="infrared_hub_slot_quota">硬件槽位:%1$d / %2$d 已占用(剩余 %3$d)</string>
<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>
@@ -34,6 +34,7 @@
<string name="infrared_back">Back</string> <string name="infrared_back">Back</string>
<string name="infrared_remote_title">Infrared Remote</string> <string name="infrared_remote_title">Infrared Remote</string>
<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_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>
@@ -133,4 +133,31 @@ class InfraredHubViewModelTest {
job.cancel() 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()
}
} }
@@ -19,7 +19,8 @@ class CreateVirtualRemote(
displayName: String, displayName: String,
type: RemoteDeviceType, type: RemoteDeviceType,
brandModel: String? = null, brandModel: String? = null,
transmissionMode: TransmissionMode, transmissionMode: TransmissionMode = if (type == RemoteDeviceType.CUSTOM) TransmissionMode.STATIC_SLOTS else TransmissionMode.DYNAMIC_SCRATCH,
layoutStyle: PanelLayoutStyle? = null,
): DomainResult<VirtualRemoteId> { ): DomainResult<VirtualRemoteId> {
val trimmedName = displayName.trim() val trimmedName = displayName.trim()
if (trimmedName.isBlank()) { if (trimmedName.isBlank()) {
@@ -28,12 +29,14 @@ class CreateVirtualRemote(
) )
} }
val templates = PresetCodeLibrary.getTemplatesForType(type, brandModel) val resolvedLayout = layoutStyle ?: VirtualRemote.defaultLayoutFor(type)
val quotaResult = repository.getKeyBindings(VirtualRemoteId("dummy")) // or compute from quota val resolvedTransMode = if (type == RemoteDeviceType.CUSTOM) {
// Obtain slot quota TransmissionMode.STATIC_SLOTS
// We calculate needed static slots: } else {
val neededStaticSlots = if (transmissionMode == TransmissionMode.STATIC_SLOTS) templates.size else 0 transmissionMode
}
val templates = PresetCodeLibrary.getTemplatesForType(type, brandModel, resolvedLayout)
val remoteId = VirtualRemoteId(UUID.randomUUID().toString().replace("-", "").take(16)) val remoteId = VirtualRemoteId(UUID.randomUUID().toString().replace("-", "").take(16))
val now = clock.now() val now = clock.now()
@@ -46,7 +49,8 @@ class CreateVirtualRemote(
displayName = trimmedName, displayName = trimmedName,
type = type, type = type,
brandModel = brandModel, brandModel = brandModel,
transmissionMode = transmissionMode, layoutStyle = resolvedLayout,
transmissionMode = resolvedTransMode,
dynamicScratchSlot = VirtualRemote.DEFAULT_DYNAMIC_SCRATCH_SLOT, dynamicScratchSlot = VirtualRemote.DEFAULT_DYNAMIC_SCRATCH_SLOT,
acState = initialAcState, acState = initialAcState,
createdAtEpochMillis = now, createdAtEpochMillis = now,
@@ -54,9 +58,7 @@ class CreateVirtualRemote(
) )
// Build key bindings // Build key bindings
val bindings = if (transmissionMode == TransmissionMode.STATIC_SLOTS) { val bindings = if (resolvedTransMode == TransmissionMode.STATIC_SLOTS) {
// Find currently used slots by querying quota
// For now, assign slots incrementally or check
templates.mapIndexed { index, template -> templates.mapIndexed { index, template ->
RemoteKeyBinding( RemoteKeyBinding(
id = UUID.randomUUID().toString(), id = UUID.randomUUID().toString(),
@@ -64,7 +66,7 @@ class CreateVirtualRemote(
profileId = profileId, profileId = profileId,
buttonId = template.buttonId, buttonId = template.buttonId,
displayName = template.displayName, displayName = template.displayName,
assignedSlot = (index + 1).toString(), // Will be allocated based on allocator assignedSlot = (index + 1).toString(),
rawHexCode = template.defaultHexCode, rawHexCode = template.defaultHexCode,
status = if (template.defaultHexCode != null) InfraredKeyStatus.READY else InfraredKeyStatus.FAILED, 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 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 -> val codesToDownload = bindings.mapNotNull { b ->
if (b.assignedSlot != null && b.rawHexCode != null) { if (b.assignedSlot != null && b.rawHexCode != null) {
InfraredCode(deviceName, b.assignedSlot, b.rawHexCode) InfraredCode(deviceName, b.assignedSlot, b.rawHexCode)
@@ -26,7 +26,7 @@ object PresetCodeLibrary {
PresetBrandOption("generic_fan", "通用风扇 (Generic Fan)", RemoteDeviceType.FAN), PresetBrandOption("generic_fan", "通用风扇 (Generic Fan)", RemoteDeviceType.FAN),
// 自定义 // 自定义
PresetBrandOption("custom_blank", "自定义学习遥控器 (Custom)", RemoteDeviceType.CUSTOM), PresetBrandOption("custom_blank", "自定义遥控器 (Custom)", RemoteDeviceType.CUSTOM),
) )
data class PresetButtonTemplate( data class PresetButtonTemplate(
@@ -35,24 +35,12 @@ object PresetCodeLibrary {
val defaultHexCode: String? = null, val defaultHexCode: String? = null,
) )
fun getTemplatesForType(type: RemoteDeviceType, presetId: String? = null): List<PresetButtonTemplate> = when (type) { fun getTemplatesForType(
RemoteDeviceType.TV -> listOf( type: RemoteDeviceType,
PresetButtonTemplate("TV_POWER", "电源", "0x00FF02FD"), presetId: String? = null,
PresetButtonTemplate("TV_MUTE", "静音", "0x00FF04FB"), layoutStyle: PanelLayoutStyle? = null,
PresetButtonTemplate("TV_SOURCE", "信号源", "0x00FF06F9"), ): List<PresetButtonTemplate> = when (type) {
PresetButtonTemplate("TV_NAV_UP", "上", "0x00FF08F7"), RemoteDeviceType.TV -> getTvTemplates(hasHexCode = true)
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"),
)
RemoteDeviceType.AIR_CONDITIONER -> listOf( RemoteDeviceType.AIR_CONDITIONER -> listOf(
PresetButtonTemplate("AC_POWER", "电源"), PresetButtonTemplate("AC_POWER", "电源"),
PresetButtonTemplate("AC_TEMP_UP", "升温"), PresetButtonTemplate("AC_TEMP_UP", "升温"),
@@ -70,12 +58,44 @@ object PresetCodeLibrary {
PresetButtonTemplate("FAN_TIMER", "定时", "0x01FE08F7"), PresetButtonTemplate("FAN_TIMER", "定时", "0x01FE08F7"),
PresetButtonTemplate("FAN_MODE", "模式", "0x01FE0AF5"), PresetButtonTemplate("FAN_MODE", "模式", "0x01FE0AF5"),
) )
RemoteDeviceType.CUSTOM -> listOf( 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_POWER", "电源"),
PresetButtonTemplate("CUSTOM_KEY_1", "按键 1"), PresetButtonTemplate("CUSTOM_KEY_1", "按键 1"),
PresetButtonTemplate("CUSTOM_KEY_2", "按键 2"), PresetButtonTemplate("CUSTOM_KEY_2", "按键 2"),
PresetButtonTemplate("CUSTOM_KEY_3", "按键 3"), PresetButtonTemplate("CUSTOM_KEY_3", "按键 3"),
PresetButtonTemplate("CUSTOM_KEY_4", "按键 4"),
) )
} }
} }
private fun getTvTemplates(hasHexCode: Boolean): List<PresetButtonTemplate> = 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<PresetButtonTemplate> = 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),
)
}