feat(infrared): add infrared control, learning, and virtual remote module #9
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package com.flagship.abox.manager.infrared.remote
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.flagship.abox.manager.domain.DeviceName
|
||||||
|
import com.flagship.abox.manager.infrared.remote.ui.InfraredRemoteScreen
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class InfraredRemoteScreenTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rendersAcPanelAndTvPanelCorrectly() {
|
||||||
|
val state = InfraredRemoteUiState(
|
||||||
|
selectedScene = RemoteScene.AIR_CONDITIONER,
|
||||||
|
savedDeviceName = DeviceName("ir-1"),
|
||||||
|
)
|
||||||
|
var clickedSpec: RemoteButtonSpec? = null
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
InfraredRemoteScreen(
|
||||||
|
state = state,
|
||||||
|
onBack = {},
|
||||||
|
onOpenSettings = {},
|
||||||
|
onSessionInvalid = {},
|
||||||
|
onSceneSelected = {},
|
||||||
|
onToggleLearningMode = {},
|
||||||
|
onButtonClick = { clickedSpec = it },
|
||||||
|
onMessageShown = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("remote_button_AC_POWER").assertIsDisplayed().performClick()
|
||||||
|
assertEquals("AC_POWER", clickedSpec?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
package com.flagship.abox.manager.infrared.remote.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus
|
||||||
|
import com.flagship.abox.manager.domain.infrared.SavedInfraredKey
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteButtonSpec
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteSceneConfig
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AcRemotePanel(
|
||||||
|
learnedSlotMap: Map<String, SavedInfraredKey>,
|
||||||
|
activeLearningSlot: String?,
|
||||||
|
sendingKeySlot: String?,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onButtonClick: (RemoteButtonSpec) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
RemoteChassis(modifier = modifier) {
|
||||||
|
// 顶部虚拟 LCD 液晶显示区
|
||||||
|
AcLcdDisplay()
|
||||||
|
|
||||||
|
// 电源与温度控制行
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// 电源按键
|
||||||
|
val powerSpec = RemoteSceneConfig.AirConditioner.POWER
|
||||||
|
val isPowerLearned = learnedSlotMap[powerSpec.defaultSlot]?.status == InfraredKeyStatus.READY
|
||||||
|
val isPowerLoading = activeLearningSlot == powerSpec.defaultSlot || sendingKeySlot == powerSpec.defaultSlot
|
||||||
|
|
||||||
|
RemoteCircleButton(
|
||||||
|
spec = powerSpec,
|
||||||
|
isLearned = isPowerLearned,
|
||||||
|
isLoading = isPowerLoading,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onClick = { onButtonClick(powerSpec) },
|
||||||
|
size = 72.dp,
|
||||||
|
containerColor = Color(0xFFE53935),
|
||||||
|
contentColor = Color.White,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 垂直胶囊温度调节器
|
||||||
|
val tempUpSpec = RemoteSceneConfig.AirConditioner.TEMP_UP
|
||||||
|
val tempDownSpec = RemoteSceneConfig.AirConditioner.TEMP_DOWN
|
||||||
|
|
||||||
|
VerticalCapsuleRocker(
|
||||||
|
upSpec = tempUpSpec,
|
||||||
|
downSpec = tempDownSpec,
|
||||||
|
isUpLearned = learnedSlotMap[tempUpSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isDownLearned = learnedSlotMap[tempDownSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isUpLoading = activeLearningSlot == tempUpSpec.defaultSlot || sendingKeySlot == tempUpSpec.defaultSlot,
|
||||||
|
isDownLoading = activeLearningSlot == tempDownSpec.defaultSlot || sendingKeySlot == tempDownSpec.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onUpClick = { onButtonClick(tempUpSpec) },
|
||||||
|
onDownClick = { onButtonClick(tempDownSpec) },
|
||||||
|
label = "温度调节",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// 功能按键网格 (模式 / 风速 / 扫风 / 定时)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
AcGridButton(RemoteSceneConfig.AirConditioner.MODE, learnedSlotMap, activeLearningSlot, sendingKeySlot, isLearningMode, onButtonClick)
|
||||||
|
AcGridButton(RemoteSceneConfig.AirConditioner.FAN, learnedSlotMap, activeLearningSlot, sendingKeySlot, isLearningMode, onButtonClick)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
AcGridButton(RemoteSceneConfig.AirConditioner.SWING, learnedSlotMap, activeLearningSlot, sendingKeySlot, isLearningMode, onButtonClick)
|
||||||
|
AcGridButton(RemoteSceneConfig.AirConditioner.TIMER, learnedSlotMap, activeLearningSlot, sendingKeySlot, isLearningMode, onButtonClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AcGridButton(
|
||||||
|
spec: RemoteButtonSpec,
|
||||||
|
learnedSlotMap: Map<String, SavedInfraredKey>,
|
||||||
|
activeLearningSlot: String?,
|
||||||
|
sendingKeySlot: String?,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onButtonClick: (RemoteButtonSpec) -> Unit,
|
||||||
|
) {
|
||||||
|
RemoteRectButton(
|
||||||
|
spec = spec,
|
||||||
|
isLearned = learnedSlotMap[spec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isLoading = activeLearningSlot == spec.defaultSlot || sendingKeySlot == spec.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onClick = { onButtonClick(spec) },
|
||||||
|
width = 120.dp,
|
||||||
|
height = 52.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AcLcdDisplay() {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(96.dp)
|
||||||
|
.shadow(4.dp, shape = RoundedCornerShape(16.dp)),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "❄️ 制冷模式",
|
||||||
|
color = Color(0xFF38BDF8),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "💨 自动风速 | ↔ 扫风",
|
||||||
|
color = Color(0xFF94A3B8),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "26°C",
|
||||||
|
color = Color(0xFF4ADE80),
|
||||||
|
fontSize = 38.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+246
@@ -0,0 +1,246 @@
|
|||||||
|
package com.flagship.abox.manager.infrared.remote.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.flagship.abox.manager.designsystem.ABoxSurface
|
||||||
|
import com.flagship.abox.manager.infrared.remote.InfraredRemoteUiState
|
||||||
|
import com.flagship.abox.manager.infrared.remote.InfraredRemoteViewModel
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteButtonSpec
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteScene
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteUiMessage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfraredRemoteRoute(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
onSessionInvalid: () -> Unit,
|
||||||
|
viewModel: InfraredRemoteViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
InfraredRemoteScreen(
|
||||||
|
state = state,
|
||||||
|
onBack = onBack,
|
||||||
|
onOpenSettings = onOpenSettings,
|
||||||
|
onSessionInvalid = onSessionInvalid,
|
||||||
|
onSceneSelected = viewModel::switchScene,
|
||||||
|
onToggleLearningMode = viewModel::toggleLearningMode,
|
||||||
|
onButtonClick = viewModel::onButtonClick,
|
||||||
|
onMessageShown = viewModel::clearMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun InfraredRemoteScreen(
|
||||||
|
state: InfraredRemoteUiState,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
onSessionInvalid: () -> Unit,
|
||||||
|
onSceneSelected: (RemoteScene) -> Unit,
|
||||||
|
onToggleLearningMode: (Boolean) -> Unit,
|
||||||
|
onButtonClick: (RemoteButtonSpec) -> Unit,
|
||||||
|
onMessageShown: () -> Unit,
|
||||||
|
) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
LaunchedEffect(state.message) {
|
||||||
|
state.message?.let { msg ->
|
||||||
|
when (msg) {
|
||||||
|
is RemoteUiMessage.Info -> {
|
||||||
|
snackbarHostState.showSnackbar(msg.text)
|
||||||
|
onMessageShown()
|
||||||
|
}
|
||||||
|
is RemoteUiMessage.Error -> {
|
||||||
|
snackbarHostState.showSnackbar(msg.text)
|
||||||
|
onMessageShown()
|
||||||
|
}
|
||||||
|
is RemoteUiMessage.SessionInvalid -> {
|
||||||
|
onSessionInvalid()
|
||||||
|
onMessageShown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "红外遥控器",
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
TextButton(onClick = onBack) {
|
||||||
|
Text("返回")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
// 学习模式开关
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (state.isLearningMode) "录制中" else "学习模式",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = if (state.isLearningMode) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
color = if (state.isLearningMode) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = state.isLearningMode,
|
||||||
|
onCheckedChange = onToggleLearningMode,
|
||||||
|
modifier = Modifier.testTag("remote_learning_mode_switch"),
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.error,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置齿轮按钮
|
||||||
|
IconButton(
|
||||||
|
onClick = onOpenSettings,
|
||||||
|
modifier = Modifier.testTag("remote_settings_button"),
|
||||||
|
) {
|
||||||
|
Text("⚙️", fontSize = 20.sp)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
) { innerPadding ->
|
||||||
|
ABoxSurface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
// 学习模式顶部横幅提示
|
||||||
|
if (state.isLearningMode) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.8f),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "🔴 红外学习模式已开启:点击任意按键即可录制信号",
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景切换 Tab (空调 | 电视)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = state.selectedScene == RemoteScene.AIR_CONDITIONER,
|
||||||
|
onClick = { onSceneSelected(RemoteScene.AIR_CONDITIONER) },
|
||||||
|
label = { Text("❄️ 空调遥控") },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.testTag("scene_tab_ac"),
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = state.selectedScene == RemoteScene.TELEVISION,
|
||||||
|
onClick = { onSceneSelected(RemoteScene.TELEVISION) },
|
||||||
|
label = { Text("📺 电视遥控") },
|
||||||
|
modifier = Modifier.testTag("scene_tab_tv"),
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对应场景遥控器面板
|
||||||
|
when (state.selectedScene) {
|
||||||
|
RemoteScene.AIR_CONDITIONER -> {
|
||||||
|
AcRemotePanel(
|
||||||
|
learnedSlotMap = state.learnedSlotMap,
|
||||||
|
activeLearningSlot = state.activeLearningSlot,
|
||||||
|
sendingKeySlot = state.sendingKeySlot,
|
||||||
|
isLearningMode = state.isLearningMode,
|
||||||
|
onButtonClick = onButtonClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RemoteScene.TELEVISION -> {
|
||||||
|
TvRemotePanel(
|
||||||
|
learnedSlotMap = state.learnedSlotMap,
|
||||||
|
activeLearningSlot = state.activeLearningSlot,
|
||||||
|
sendingKeySlot = state.sendingKeySlot,
|
||||||
|
isLearningMode = state.isLearningMode,
|
||||||
|
onButtonClick = onButtonClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+407
@@ -0,0 +1,407 @@
|
|||||||
|
package com.flagship.abox.manager.infrared.remote.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteButtonSpec
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RemoteChassis(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.shadow(12.dp, shape = RoundedCornerShape(32.dp)),
|
||||||
|
shape = RoundedCornerShape(32.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RemoteCircleButton(
|
||||||
|
spec: RemoteButtonSpec,
|
||||||
|
isLearned: Boolean,
|
||||||
|
isLoading: Boolean,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 64.dp,
|
||||||
|
containerColor: Color? = null,
|
||||||
|
contentColor: Color? = null,
|
||||||
|
) {
|
||||||
|
val defaultContainer = when {
|
||||||
|
isLearningMode -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||||
|
isLearned -> containerColor ?: MaterialTheme.colorScheme.primary
|
||||||
|
else -> MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultContent = when {
|
||||||
|
isLearningMode -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
isLearned -> contentColor ?: MaterialTheme.colorScheme.onPrimary
|
||||||
|
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val borderModifier = if (isLearningMode) {
|
||||||
|
Modifier.border(2.dp, MaterialTheme.colorScheme.primary, CircleShape)
|
||||||
|
} else if (!isLearned) {
|
||||||
|
Modifier.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), CircleShape)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.then(borderModifier)
|
||||||
|
.testTag("remote_button_${spec.id}"),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = defaultContainer,
|
||||||
|
contentColor = defaultContent,
|
||||||
|
shadowElevation = if (isLearned) 4.dp else 1.dp,
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(size * 0.45f),
|
||||||
|
strokeWidth = 2.5.dp,
|
||||||
|
color = defaultContent,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = spec.symbol,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
fontSize = if (spec.symbol.length <= 2) 22.sp else 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RemoteRectButton(
|
||||||
|
spec: RemoteButtonSpec,
|
||||||
|
isLearned: Boolean,
|
||||||
|
isLoading: Boolean,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
width: Dp = 100.dp,
|
||||||
|
height: Dp = 48.dp,
|
||||||
|
) {
|
||||||
|
val containerColor = when {
|
||||||
|
isLearningMode -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||||
|
isLearned -> MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else -> MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
|
val contentColor = when {
|
||||||
|
isLearningMode -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
isLearned -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val borderModifier = if (isLearningMode) {
|
||||||
|
Modifier.border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(16.dp))
|
||||||
|
} else if (!isLearned) {
|
||||||
|
Modifier.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), RoundedCornerShape(16.dp))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.width(width)
|
||||||
|
.height(height)
|
||||||
|
.then(borderModifier)
|
||||||
|
.testTag("remote_button_${spec.id}"),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
shadowElevation = if (isLearned) 3.dp else 1.dp,
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = contentColor,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = spec.symbol,
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalCapsuleRocker(
|
||||||
|
upSpec: RemoteButtonSpec,
|
||||||
|
downSpec: RemoteButtonSpec,
|
||||||
|
isUpLearned: Boolean,
|
||||||
|
isDownLearned: Boolean,
|
||||||
|
isUpLoading: Boolean,
|
||||||
|
isDownLoading: Boolean,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onUpClick: () -> Unit,
|
||||||
|
onDownClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
label: String = "",
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(68.dp)
|
||||||
|
.height(136.dp)
|
||||||
|
.shadow(4.dp, shape = RoundedCornerShape(34.dp)),
|
||||||
|
shape = RoundedCornerShape(34.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
// Top half
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(topStart = 34.dp, topEnd = 34.dp))
|
||||||
|
.background(
|
||||||
|
if (isLearningMode) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||||
|
else if (isUpLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||||
|
else Color.Transparent,
|
||||||
|
)
|
||||||
|
.clickable(onClick = onUpClick)
|
||||||
|
.testTag("remote_button_${upSpec.id}"),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isUpLoading) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = upSpec.symbol,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
|
||||||
|
// Bottom half
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(bottomStart = 34.dp, bottomEnd = 34.dp))
|
||||||
|
.background(
|
||||||
|
if (isLearningMode) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||||
|
else if (isDownLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||||
|
else Color.Transparent,
|
||||||
|
)
|
||||||
|
.clickable(onClick = onDownClick)
|
||||||
|
.testTag("remote_button_${downSpec.id}"),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isDownLoading) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = downSpec.symbol,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (label.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RemoteDpadWheel(
|
||||||
|
upSpec: RemoteButtonSpec,
|
||||||
|
downSpec: RemoteButtonSpec,
|
||||||
|
leftSpec: RemoteButtonSpec,
|
||||||
|
rightSpec: RemoteButtonSpec,
|
||||||
|
okSpec: RemoteButtonSpec,
|
||||||
|
isUpLearned: Boolean,
|
||||||
|
isDownLearned: Boolean,
|
||||||
|
isLeftLearned: Boolean,
|
||||||
|
isRightLearned: Boolean,
|
||||||
|
isOkLearned: Boolean,
|
||||||
|
isUpLoading: Boolean,
|
||||||
|
isDownLoading: Boolean,
|
||||||
|
isLeftLoading: Boolean,
|
||||||
|
isRightLoading: Boolean,
|
||||||
|
isOkLoading: Boolean,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onUpClick: () -> Unit,
|
||||||
|
onDownClick: () -> Unit,
|
||||||
|
onLeftClick: () -> Unit,
|
||||||
|
onRightClick: () -> Unit,
|
||||||
|
onOkClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
wheelSize: Dp = 190.dp,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.size(wheelSize)
|
||||||
|
.shadow(6.dp, shape = CircleShape),
|
||||||
|
shape = CircleShape,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
// Up Button
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(top = 10.dp)
|
||||||
|
.size(54.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onUpClick)
|
||||||
|
.testTag("remote_button_${upSpec.id}"),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isUpLoading) CircularProgressIndicator(modifier = Modifier.size(18.dp))
|
||||||
|
else Text(upSpec.symbol, style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down Button
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
.size(54.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onDownClick)
|
||||||
|
.testTag("remote_button_${downSpec.id}"),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isDownLoading) CircularProgressIndicator(modifier = Modifier.size(18.dp))
|
||||||
|
else Text(downSpec.symbol, style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left Button
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.size(54.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onLeftClick)
|
||||||
|
.testTag("remote_button_${leftSpec.id}"),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isLeftLoading) CircularProgressIndicator(modifier = Modifier.size(18.dp))
|
||||||
|
else Text(leftSpec.symbol, style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Button
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.size(54.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onRightClick)
|
||||||
|
.testTag("remote_button_${rightSpec.id}"),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (isRightLoading) CircularProgressIndicator(modifier = Modifier.size(18.dp))
|
||||||
|
else Text(rightSpec.symbol, style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center OK Button
|
||||||
|
RemoteCircleButton(
|
||||||
|
spec = okSpec,
|
||||||
|
isLearned = isOkLearned,
|
||||||
|
isLoading = isOkLoading,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onClick = onOkClick,
|
||||||
|
size = 62.dp,
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package com.flagship.abox.manager.infrared.remote.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus
|
||||||
|
import com.flagship.abox.manager.domain.infrared.SavedInfraredKey
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteButtonSpec
|
||||||
|
import com.flagship.abox.manager.infrared.remote.RemoteSceneConfig
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvRemotePanel(
|
||||||
|
learnedSlotMap: Map<String, SavedInfraredKey>,
|
||||||
|
activeLearningSlot: String?,
|
||||||
|
sendingKeySlot: String?,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onButtonClick: (RemoteButtonSpec) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
RemoteChassis(modifier = modifier) {
|
||||||
|
// 顶部电源与静音行
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val powerSpec = RemoteSceneConfig.Television.POWER
|
||||||
|
RemoteCircleButton(
|
||||||
|
spec = powerSpec,
|
||||||
|
isLearned = learnedSlotMap[powerSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isLoading = activeLearningSlot == powerSpec.defaultSlot || sendingKeySlot == powerSpec.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onClick = { onButtonClick(powerSpec) },
|
||||||
|
size = 56.dp,
|
||||||
|
containerColor = Color(0xFFE53935),
|
||||||
|
contentColor = Color.White,
|
||||||
|
)
|
||||||
|
|
||||||
|
val muteSpec = RemoteSceneConfig.Television.MUTE
|
||||||
|
RemoteCircleButton(
|
||||||
|
spec = muteSpec,
|
||||||
|
isLearned = learnedSlotMap[muteSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isLoading = activeLearningSlot == muteSpec.defaultSlot || sendingKeySlot == muteSpec.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onClick = { onButtonClick(muteSpec) },
|
||||||
|
size = 56.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中心圆形方向轮盘 (D-Pad Wheel)
|
||||||
|
val upSpec = RemoteSceneConfig.Television.NAV_UP
|
||||||
|
val downSpec = RemoteSceneConfig.Television.NAV_DOWN
|
||||||
|
val leftSpec = RemoteSceneConfig.Television.NAV_LEFT
|
||||||
|
val rightSpec = RemoteSceneConfig.Television.NAV_RIGHT
|
||||||
|
val okSpec = RemoteSceneConfig.Television.OK
|
||||||
|
|
||||||
|
RemoteDpadWheel(
|
||||||
|
upSpec = upSpec,
|
||||||
|
downSpec = downSpec,
|
||||||
|
leftSpec = leftSpec,
|
||||||
|
rightSpec = rightSpec,
|
||||||
|
okSpec = okSpec,
|
||||||
|
isUpLearned = learnedSlotMap[upSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isDownLearned = learnedSlotMap[downSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isLeftLearned = learnedSlotMap[leftSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isRightLearned = learnedSlotMap[rightSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isOkLearned = learnedSlotMap[okSpec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isUpLoading = activeLearningSlot == upSpec.defaultSlot || sendingKeySlot == upSpec.defaultSlot,
|
||||||
|
isDownLoading = activeLearningSlot == downSpec.defaultSlot || sendingKeySlot == downSpec.defaultSlot,
|
||||||
|
isLeftLoading = activeLearningSlot == leftSpec.defaultSlot || sendingKeySlot == leftSpec.defaultSlot,
|
||||||
|
isRightLoading = activeLearningSlot == rightSpec.defaultSlot || sendingKeySlot == rightSpec.defaultSlot,
|
||||||
|
isOkLoading = activeLearningSlot == okSpec.defaultSlot || sendingKeySlot == okSpec.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onUpClick = { onButtonClick(upSpec) },
|
||||||
|
onDownClick = { onButtonClick(downSpec) },
|
||||||
|
onLeftClick = { onButtonClick(leftSpec) },
|
||||||
|
onRightClick = { onButtonClick(rightSpec) },
|
||||||
|
onOkClick = { onButtonClick(okSpec) },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 系统功能按键行 (返回 / 主页 / 菜单)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
TvActionButton(RemoteSceneConfig.Television.BACK, learnedSlotMap, activeLearningSlot, sendingKeySlot, isLearningMode, onButtonClick)
|
||||||
|
TvActionButton(RemoteSceneConfig.Television.HOME, learnedSlotMap, activeLearningSlot, sendingKeySlot, isLearningMode, onButtonClick)
|
||||||
|
TvActionButton(RemoteSceneConfig.Television.MENU, learnedSlotMap, activeLearningSlot, sendingKeySlot, isLearningMode, onButtonClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// 音量与频道双摇杆胶囊
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// 音量控制
|
||||||
|
val volUp = RemoteSceneConfig.Television.VOL_UP
|
||||||
|
val volDown = RemoteSceneConfig.Television.VOL_DOWN
|
||||||
|
VerticalCapsuleRocker(
|
||||||
|
upSpec = volUp,
|
||||||
|
downSpec = volDown,
|
||||||
|
isUpLearned = learnedSlotMap[volUp.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isDownLearned = learnedSlotMap[volDown.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isUpLoading = activeLearningSlot == volUp.defaultSlot || sendingKeySlot == volUp.defaultSlot,
|
||||||
|
isDownLoading = activeLearningSlot == volDown.defaultSlot || sendingKeySlot == volDown.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onUpClick = { onButtonClick(volUp) },
|
||||||
|
onDownClick = { onButtonClick(volDown) },
|
||||||
|
label = "音量 VOL",
|
||||||
|
)
|
||||||
|
|
||||||
|
// 频道控制
|
||||||
|
val chUp = RemoteSceneConfig.Television.CH_UP
|
||||||
|
val chDown = RemoteSceneConfig.Television.CH_DOWN
|
||||||
|
VerticalCapsuleRocker(
|
||||||
|
upSpec = chUp,
|
||||||
|
downSpec = chDown,
|
||||||
|
isUpLearned = learnedSlotMap[chUp.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isDownLearned = learnedSlotMap[chDown.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isUpLoading = activeLearningSlot == chUp.defaultSlot || sendingKeySlot == chUp.defaultSlot,
|
||||||
|
isDownLoading = activeLearningSlot == chDown.defaultSlot || sendingKeySlot == chDown.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onUpClick = { onButtonClick(chUp) },
|
||||||
|
onDownClick = { onButtonClick(chDown) },
|
||||||
|
label = "频道 CH",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TvActionButton(
|
||||||
|
spec: RemoteButtonSpec,
|
||||||
|
learnedSlotMap: Map<String, SavedInfraredKey>,
|
||||||
|
activeLearningSlot: String?,
|
||||||
|
sendingKeySlot: String?,
|
||||||
|
isLearningMode: Boolean,
|
||||||
|
onButtonClick: (RemoteButtonSpec) -> Unit,
|
||||||
|
) {
|
||||||
|
RemoteRectButton(
|
||||||
|
spec = spec,
|
||||||
|
isLearned = learnedSlotMap[spec.defaultSlot]?.status == InfraredKeyStatus.READY,
|
||||||
|
isLoading = activeLearningSlot == spec.defaultSlot || sendingKeySlot == spec.defaultSlot,
|
||||||
|
isLearningMode = isLearningMode,
|
||||||
|
onClick = { onButtonClick(spec) },
|
||||||
|
width = 84.dp,
|
||||||
|
height = 44.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user