feat(infrared): add infrared control, learning, and virtual remote module #9
+5
-8
@@ -6,6 +6,7 @@ import com.flagship.abox.manager.domain.infrared.InfraredKeyRef
|
||||
import com.flagship.abox.manager.domain.infrared.LearnInfraredKey
|
||||
import com.flagship.abox.manager.domain.infrared.RetryInfraredKeyLearning
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class InfraredLearningCoordinator(
|
||||
@@ -18,15 +19,11 @@ class InfraredLearningCoordinator(
|
||||
deviceName: DeviceName,
|
||||
displayName: String,
|
||||
sdkKey: String? = null,
|
||||
) {
|
||||
externalScope.launch {
|
||||
learnInfraredKey(profileId, deviceName, displayName, sdkKey)
|
||||
}
|
||||
): Job = externalScope.launch {
|
||||
learnInfraredKey(profileId, deviceName, displayName, sdkKey)
|
||||
}
|
||||
|
||||
fun retry(ref: InfraredKeyRef) {
|
||||
externalScope.launch {
|
||||
retryInfraredKeyLearning(ref)
|
||||
}
|
||||
fun retry(ref: InfraredKeyRef): Job = externalScope.launch {
|
||||
retryInfraredKeyLearning(ref)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -148,7 +148,9 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
|
||||
fun toggleLearningMode(enabled: Boolean) {
|
||||
isLearningModeFlow.value = enabled
|
||||
if (enabled) {
|
||||
if (!enabled) {
|
||||
activeLearningSlotFlow.value = null
|
||||
} else {
|
||||
messageFlow.value = RemoteUiMessage.Info("🔴 学习录制模式已开启:点击任意按键即可开始录制")
|
||||
}
|
||||
}
|
||||
@@ -162,15 +164,26 @@ class InfraredRemoteViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
if (isLearningModeFlow.value) {
|
||||
if (activeLearningSlotFlow.value != null) {
|
||||
return
|
||||
}
|
||||
activeLearningSlotFlow.value = spec.defaultSlot
|
||||
learningCoordinator.learn(
|
||||
val job = learningCoordinator.learn(
|
||||
profileId = currentProfile,
|
||||
deviceName = savedDevice,
|
||||
displayName = spec.displayName,
|
||||
sdkKey = spec.defaultSlot,
|
||||
)
|
||||
job.invokeOnCompletion {
|
||||
if (activeLearningSlotFlow.value == spec.defaultSlot) {
|
||||
activeLearningSlotFlow.value = null
|
||||
}
|
||||
}
|
||||
messageFlow.value = RemoteUiMessage.Info("正在录制【${spec.displayName}】(Slot ${spec.defaultSlot}),请按遥控器按键...")
|
||||
} else {
|
||||
if (sendingKeySlotFlow.value != null) {
|
||||
return
|
||||
}
|
||||
val key = state.value.learnedSlotMap[spec.defaultSlot]
|
||||
if (key == null || key.status != InfraredKeyStatus.READY) {
|
||||
messageFlow.value = RemoteUiMessage.Error("按键【${spec.displayName}】尚未学习,请开启右上角学习模式进行录制")
|
||||
|
||||
+28
-6
@@ -108,6 +108,7 @@ fun RemoteCircleButton(
|
||||
color = defaultContainer,
|
||||
contentColor = defaultContent,
|
||||
shadowElevation = if (isLearned) 4.dp else 1.dp,
|
||||
enabled = !isLoading,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Box(
|
||||
@@ -174,6 +175,7 @@ fun RemoteRectButton(
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
shadowElevation = if (isLearned) 3.dp else 1.dp,
|
||||
enabled = !isLoading,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Box(
|
||||
@@ -240,7 +242,7 @@ fun VerticalCapsuleRocker(
|
||||
else if (isUpLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||
else Color.Transparent,
|
||||
)
|
||||
.clickable(onClick = onUpClick)
|
||||
.clickable(enabled = !isUpLoading, onClick = onUpClick)
|
||||
.testTag("remote_button_${upSpec.id}"),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@@ -267,7 +269,7 @@ fun VerticalCapsuleRocker(
|
||||
else if (isDownLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||
else Color.Transparent,
|
||||
)
|
||||
.clickable(onClick = onDownClick)
|
||||
.clickable(enabled = !isDownLoading, onClick = onDownClick)
|
||||
.testTag("remote_button_${downSpec.id}"),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@@ -338,7 +340,12 @@ fun RemoteDpadWheel(
|
||||
.padding(top = 10.dp)
|
||||
.size(54.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onUpClick)
|
||||
.background(
|
||||
if (isLearningMode) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||
else if (isUpLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||
else Color.Transparent,
|
||||
)
|
||||
.clickable(enabled = !isUpLoading, onClick = onUpClick)
|
||||
.testTag("remote_button_${upSpec.id}"),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@@ -353,7 +360,12 @@ fun RemoteDpadWheel(
|
||||
.padding(bottom = 10.dp)
|
||||
.size(54.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onDownClick)
|
||||
.background(
|
||||
if (isLearningMode) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||
else if (isDownLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||
else Color.Transparent,
|
||||
)
|
||||
.clickable(enabled = !isDownLoading, onClick = onDownClick)
|
||||
.testTag("remote_button_${downSpec.id}"),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@@ -368,7 +380,12 @@ fun RemoteDpadWheel(
|
||||
.padding(start = 10.dp)
|
||||
.size(54.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onLeftClick)
|
||||
.background(
|
||||
if (isLearningMode) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||
else if (isLeftLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||
else Color.Transparent,
|
||||
)
|
||||
.clickable(enabled = !isLeftLoading, onClick = onLeftClick)
|
||||
.testTag("remote_button_${leftSpec.id}"),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@@ -383,7 +400,12 @@ fun RemoteDpadWheel(
|
||||
.padding(end = 10.dp)
|
||||
.size(54.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onRightClick)
|
||||
.background(
|
||||
if (isLearningMode) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
|
||||
else if (isRightLearned) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||
else Color.Transparent,
|
||||
)
|
||||
.clickable(enabled = !isRightLoading, onClick = onRightClick)
|
||||
.testTag("remote_button_${rightSpec.id}"),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
|
||||
+47
@@ -133,6 +133,53 @@ class InfraredRemoteViewModelTest {
|
||||
|
||||
assertEquals(1, gateway.learnedKeys.size)
|
||||
assertEquals("1", gateway.learnedKeys.first().key)
|
||||
assertEquals(null, vm.state.value.activeLearningSlot)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disablingLearningModeClearsActiveLearningSlot() = testScope.runTest {
|
||||
deviceRepo.setDeviceName(fakeProfile, device)
|
||||
val vm = createViewModel()
|
||||
val job = launch { vm.state.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
vm.toggleLearningMode(true)
|
||||
vm.onButtonClick(RemoteSceneConfig.AirConditioner.POWER)
|
||||
vm.toggleLearningMode(false)
|
||||
|
||||
assertEquals(null, vm.state.value.activeLearningSlot)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicateSendButtonClickIsIgnoredWhileSending() = testScope.runTest {
|
||||
deviceRepo.setDeviceName(fakeProfile, device)
|
||||
val readyKey = SavedInfraredKey(
|
||||
id = InfraredKeyId("k1"),
|
||||
profileId = fakeProfile,
|
||||
deviceName = device,
|
||||
displayName = "空调-电源",
|
||||
sdkKey = "1",
|
||||
status = InfraredKeyStatus.READY,
|
||||
lastErrorCode = null,
|
||||
lastErrorDetail = null,
|
||||
createdAtEpochMillis = 100L,
|
||||
updatedAtEpochMillis = 100L,
|
||||
)
|
||||
keyRepo.insert(readyKey)
|
||||
|
||||
val vm = createViewModel()
|
||||
val job = launch { vm.state.collect {} }
|
||||
advanceUntilIdle()
|
||||
|
||||
vm.onButtonClick(RemoteSceneConfig.AirConditioner.POWER)
|
||||
vm.onButtonClick(RemoteSceneConfig.AirConditioner.POWER)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, gateway.sentKeys.size)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user