feat(infrared): choose layout template at creation time and improve grid key management
This commit is contained in:
+172
-88
@@ -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"),
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
@@ -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("已成功创建遥控器"))
|
||||||
|
|||||||
+48
-66
@@ -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>
|
||||||
|
|||||||
+27
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-12
@@ -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)
|
||||||
|
|||||||
+41
-21
@@ -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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user