feat: add infrared control screen
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package com.flagship.abox.manager.infrared.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
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.domain.ProviderProfileId
|
||||||
|
import com.flagship.abox.manager.domain.infrared.InfraredKeyId
|
||||||
|
import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus
|
||||||
|
import com.flagship.abox.manager.domain.infrared.SavedInfraredKey
|
||||||
|
import com.flagship.abox.manager.infrared.runtime.InfraredRuntimeMode
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class InfraredControlScreenTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun displaysModeAndDeviceInformation() {
|
||||||
|
val state = InfraredControlUiState(
|
||||||
|
runtimeMode = InfraredRuntimeMode.FAKE,
|
||||||
|
effectiveProfileId = ProviderProfileId("abox:infrared-fake"),
|
||||||
|
savedDeviceName = DeviceName("ir-living"),
|
||||||
|
loading = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
InfraredControlScreen(
|
||||||
|
state = state,
|
||||||
|
onBack = {},
|
||||||
|
onSessionInvalid = {},
|
||||||
|
onDeviceNameChanged = {},
|
||||||
|
onSaveDeviceName = {},
|
||||||
|
onLearn = {},
|
||||||
|
onRetry = {},
|
||||||
|
onSend = {},
|
||||||
|
onRename = { _, _ -> },
|
||||||
|
onDelete = {},
|
||||||
|
onModeChanged = {},
|
||||||
|
onMessageShown = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("infrared_mode_label").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("infrared_device_name_input").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("infrared_save_device").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("infrared_learn_new_key").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rendersKeysWithCorrectActionButtons() {
|
||||||
|
val readyKey = SavedInfraredKey(
|
||||||
|
id = InfraredKeyId("k-ready"),
|
||||||
|
profileId = ProviderProfileId("abox"),
|
||||||
|
deviceName = DeviceName("ir-1"),
|
||||||
|
displayName = "Power Key",
|
||||||
|
sdkKey = "app_ir_kready",
|
||||||
|
status = InfraredKeyStatus.READY,
|
||||||
|
lastErrorCode = null,
|
||||||
|
lastErrorDetail = null,
|
||||||
|
createdAtEpochMillis = 100L,
|
||||||
|
updatedAtEpochMillis = 100L,
|
||||||
|
)
|
||||||
|
val failedKey = SavedInfraredKey(
|
||||||
|
id = InfraredKeyId("k-failed"),
|
||||||
|
profileId = ProviderProfileId("abox"),
|
||||||
|
deviceName = DeviceName("ir-1"),
|
||||||
|
displayName = "Volume Up",
|
||||||
|
sdkKey = "app_ir_kfailed",
|
||||||
|
status = InfraredKeyStatus.FAILED,
|
||||||
|
lastErrorCode = "20206",
|
||||||
|
lastErrorDetail = null,
|
||||||
|
createdAtEpochMillis = 200L,
|
||||||
|
updatedAtEpochMillis = 200L,
|
||||||
|
)
|
||||||
|
|
||||||
|
val state = InfraredControlUiState(
|
||||||
|
runtimeMode = InfraredRuntimeMode.REAL_ABSDK,
|
||||||
|
savedDeviceName = DeviceName("ir-1"),
|
||||||
|
keys = listOf(readyKey, failedKey),
|
||||||
|
loading = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
var sentKeyId: InfraredKeyId? = null
|
||||||
|
var retriedKeyId: InfraredKeyId? = null
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
InfraredControlScreen(
|
||||||
|
state = state,
|
||||||
|
onBack = {},
|
||||||
|
onSessionInvalid = {},
|
||||||
|
onDeviceNameChanged = {},
|
||||||
|
onSaveDeviceName = {},
|
||||||
|
onLearn = {},
|
||||||
|
onRetry = { retriedKeyId = it },
|
||||||
|
onSend = { sentKeyId = it },
|
||||||
|
onRename = { _, _ -> },
|
||||||
|
onDelete = {},
|
||||||
|
onModeChanged = {},
|
||||||
|
onMessageShown = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("infrared_key_k-ready").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("infrared_send_k-ready").performClick()
|
||||||
|
assertEquals(InfraredKeyId("k-ready"), sentKeyId)
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("infrared_key_k-failed").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("infrared_retry_k-failed").performClick()
|
||||||
|
assertEquals(InfraredKeyId("k-failed"), retriedKeyId)
|
||||||
|
}
|
||||||
|
}
|
||||||
+456
@@ -0,0 +1,456 @@
|
|||||||
|
package com.flagship.abox.manager.infrared.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.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
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.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.flagship.abox.manager.designsystem.ABoxButton
|
||||||
|
import com.flagship.abox.manager.designsystem.ABoxSurface
|
||||||
|
import com.flagship.abox.manager.domain.infrared.InfraredKeyId
|
||||||
|
import com.flagship.abox.manager.domain.infrared.InfraredKeyStatus
|
||||||
|
import com.flagship.abox.manager.domain.infrared.SavedInfraredKey
|
||||||
|
import com.flagship.abox.manager.infrared.R
|
||||||
|
import com.flagship.abox.manager.infrared.runtime.InfraredRuntimeMode
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfraredControlRoute(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSessionInvalid: () -> Unit,
|
||||||
|
viewModel: InfraredControlViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
InfraredControlScreen(
|
||||||
|
state = state,
|
||||||
|
onBack = onBack,
|
||||||
|
onSessionInvalid = onSessionInvalid,
|
||||||
|
onDeviceNameChanged = viewModel::onDeviceNameChanged,
|
||||||
|
onSaveDeviceName = viewModel::saveDeviceName,
|
||||||
|
onLearn = viewModel::learnKey,
|
||||||
|
onRetry = viewModel::retryLearning,
|
||||||
|
onSend = viewModel::sendKey,
|
||||||
|
onRename = viewModel::renameKey,
|
||||||
|
onDelete = viewModel::deleteKey,
|
||||||
|
onModeChanged = viewModel::setRuntimeMode,
|
||||||
|
onMessageShown = viewModel::clearMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun InfraredControlScreen(
|
||||||
|
state: InfraredControlUiState,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSessionInvalid: () -> Unit,
|
||||||
|
onDeviceNameChanged: (String) -> Unit,
|
||||||
|
onSaveDeviceName: () -> Unit,
|
||||||
|
onLearn: (String) -> Unit,
|
||||||
|
onRetry: (InfraredKeyId) -> Unit,
|
||||||
|
onSend: (InfraredKeyId) -> Unit,
|
||||||
|
onRename: (InfraredKeyId, String) -> Unit,
|
||||||
|
onDelete: (InfraredKeyId) -> Unit,
|
||||||
|
onModeChanged: (InfraredRuntimeMode) -> Unit,
|
||||||
|
onMessageShown: () -> Unit,
|
||||||
|
) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
var showLearnDialog by remember { mutableStateOf(false) }
|
||||||
|
var keyToRename by remember { mutableStateOf<SavedInfraredKey?>(null) }
|
||||||
|
var keyToDelete by remember { mutableStateOf<SavedInfraredKey?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(state.message) {
|
||||||
|
state.message?.let { msg ->
|
||||||
|
when (msg) {
|
||||||
|
is InfraredUiMessage.Info -> {
|
||||||
|
snackbarHostState.showSnackbar(msg.text)
|
||||||
|
onMessageShown()
|
||||||
|
}
|
||||||
|
is InfraredUiMessage.Error -> {
|
||||||
|
snackbarHostState.showSnackbar(msg.text)
|
||||||
|
onMessageShown()
|
||||||
|
}
|
||||||
|
is InfraredUiMessage.SessionInvalid -> {
|
||||||
|
onSessionInvalid()
|
||||||
|
onMessageShown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.infrared_title)) },
|
||||||
|
navigationIcon = {
|
||||||
|
TextButton(onClick = onBack) {
|
||||||
|
Text(stringResource(R.string.infrared_back))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
) { innerPadding ->
|
||||||
|
ABoxSurface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
// Mode indicator and toggle
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val modeText = if (state.runtimeMode == InfraredRuntimeMode.FAKE) {
|
||||||
|
stringResource(R.string.infrared_mode_fake)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.infrared_mode_real)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = modeText,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.testTag("infrared_mode_label"),
|
||||||
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
val target = if (state.runtimeMode == InfraredRuntimeMode.FAKE) {
|
||||||
|
InfraredRuntimeMode.REAL_ABSDK
|
||||||
|
} else {
|
||||||
|
InfraredRuntimeMode.FAKE
|
||||||
|
}
|
||||||
|
onModeChanged(target)
|
||||||
|
},
|
||||||
|
enabled = !state.hasActiveLearning,
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.infrared_switch_mode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device name configuration card
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
val saved = state.savedDeviceName
|
||||||
|
Text(
|
||||||
|
text = if (saved != null) {
|
||||||
|
stringResource(R.string.infrared_saved_device, saved.value)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.infrared_no_device_saved)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.deviceNameInput,
|
||||||
|
onValueChange = onDeviceNameChanged,
|
||||||
|
label = { Text(stringResource(R.string.infrared_device_name_label)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.infrared_device_name_hint)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag("infrared_device_name_input"),
|
||||||
|
)
|
||||||
|
ABoxButton(
|
||||||
|
onClick = onSaveDeviceName,
|
||||||
|
modifier = Modifier.testTag("infrared_save_device"),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.infrared_save_device))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Learned Keys Section
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.infrared_keys_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
ABoxButton(
|
||||||
|
onClick = { showLearnDialog = true },
|
||||||
|
enabled = state.savedDeviceName != null,
|
||||||
|
modifier = Modifier.testTag("infrared_learn_new_key"),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.infrared_learn_new_key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.keys.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.infrared_no_keys),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(state.keys, key = { it.id.value }) { key ->
|
||||||
|
InfraredKeyRow(
|
||||||
|
key = key,
|
||||||
|
isSending = state.sendingKeyIds.contains(key.id),
|
||||||
|
onSend = { onSend(key.id) },
|
||||||
|
onRetry = { onRetry(key.id) },
|
||||||
|
onRename = { keyToRename = key },
|
||||||
|
onDelete = { keyToDelete = key },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLearnDialog) {
|
||||||
|
var keyName by remember { mutableStateOf("") }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showLearnDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.infrared_learn_dialog_title)) },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(stringResource(R.string.infrared_learn_dialog_prompt))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = keyName,
|
||||||
|
onValueChange = { keyName = it },
|
||||||
|
label = { Text(stringResource(R.string.infrared_learn_dialog_input_label)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onLearn(keyName)
|
||||||
|
showLearnDialog = false
|
||||||
|
},
|
||||||
|
enabled = keyName.isNotBlank(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.infrared_learn_dialog_start))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showLearnDialog = false }) {
|
||||||
|
Text(stringResource(R.string.infrared_dialog_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyToRename?.let { targetKey ->
|
||||||
|
var newName by remember { mutableStateOf(targetKey.displayName) }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { keyToRename = null },
|
||||||
|
title = { Text(stringResource(R.string.infrared_rename_dialog_title)) },
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newName,
|
||||||
|
onValueChange = { newName = it },
|
||||||
|
label = { Text(stringResource(R.string.infrared_rename_dialog_input_label)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onRename(targetKey.id, newName)
|
||||||
|
keyToRename = null
|
||||||
|
},
|
||||||
|
enabled = newName.isNotBlank(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.infrared_dialog_confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { keyToRename = null }) {
|
||||||
|
Text(stringResource(R.string.infrared_dialog_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyToDelete?.let { targetKey ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { keyToDelete = null },
|
||||||
|
title = { Text(stringResource(R.string.infrared_delete_dialog_title)) },
|
||||||
|
text = { Text(stringResource(R.string.infrared_delete_dialog_message)) },
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onDelete(targetKey.id)
|
||||||
|
keyToDelete = null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.infrared_dialog_confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { keyToDelete = null }) {
|
||||||
|
Text(stringResource(R.string.infrared_dialog_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfraredKeyRow(
|
||||||
|
key: SavedInfraredKey,
|
||||||
|
isSending: Boolean,
|
||||||
|
onSend: () -> Unit,
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onRename: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag("infrared_key_${key.id.value}"),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = key.displayName,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "SDK: ${key.sdkKey}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusLabel = when (key.status) {
|
||||||
|
InfraredKeyStatus.LEARNING -> stringResource(R.string.infrared_status_learning)
|
||||||
|
InfraredKeyStatus.READY -> stringResource(R.string.infrared_status_ready)
|
||||||
|
InfraredKeyStatus.FAILED -> stringResource(R.string.infrared_status_failed)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = statusLabel,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = when (key.status) {
|
||||||
|
InfraredKeyStatus.LEARNING -> MaterialTheme.colorScheme.primary
|
||||||
|
InfraredKeyStatus.READY -> MaterialTheme.colorScheme.tertiary
|
||||||
|
InfraredKeyStatus.FAILED -> MaterialTheme.colorScheme.error
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (key.status == InfraredKeyStatus.READY) {
|
||||||
|
Button(
|
||||||
|
onClick = onSend,
|
||||||
|
enabled = !isSending,
|
||||||
|
modifier = Modifier.testTag("infrared_send_${key.id.value}"),
|
||||||
|
) {
|
||||||
|
if (isSending) {
|
||||||
|
Text(stringResource(R.string.infrared_sending))
|
||||||
|
} else {
|
||||||
|
Text(stringResource(R.string.infrared_send))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.status == InfraredKeyStatus.FAILED) {
|
||||||
|
Button(
|
||||||
|
onClick = onRetry,
|
||||||
|
modifier = Modifier.testTag("infrared_retry_${key.id.value}"),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.infrared_retry))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.status != InfraredKeyStatus.LEARNING) {
|
||||||
|
OutlinedButton(onClick = onRename) {
|
||||||
|
Text(stringResource(R.string.infrared_rename))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
OutlinedButton(onClick = onDelete) {
|
||||||
|
Text(stringResource(R.string.infrared_delete))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.height(24.dp).width(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="infrared_title">红外控制</string>
|
||||||
|
<string name="infrared_mode_fake">模式:模拟 / Fake</string>
|
||||||
|
<string name="infrared_mode_real">模式:真实 ABSDK</string>
|
||||||
|
<string name="infrared_switch_mode">切换模式</string>
|
||||||
|
<string name="infrared_device_name_label">红外设备名称</string>
|
||||||
|
<string name="infrared_device_name_hint">例如:ir-living</string>
|
||||||
|
<string name="infrared_saved_device">已保存设备:%1$s</string>
|
||||||
|
<string name="infrared_no_device_saved">未配置红外设备</string>
|
||||||
|
<string name="infrared_save_device">保存设备名</string>
|
||||||
|
<string name="infrared_keys_title">已学习按键</string>
|
||||||
|
<string name="infrared_no_keys">暂无保存的红外按键</string>
|
||||||
|
<string name="infrared_learn_new_key">学习新按键</string>
|
||||||
|
<string name="infrared_status_learning">学习中…</string>
|
||||||
|
<string name="infrared_status_ready">就绪</string>
|
||||||
|
<string name="infrared_status_failed">失败</string>
|
||||||
|
<string name="infrared_send">发射</string>
|
||||||
|
<string name="infrared_sending">发射中…</string>
|
||||||
|
<string name="infrared_retry">重试</string>
|
||||||
|
<string name="infrared_rename">重命名</string>
|
||||||
|
<string name="infrared_delete">删除</string>
|
||||||
|
<string name="infrared_learn_dialog_title">学习红外按键</string>
|
||||||
|
<string name="infrared_learn_dialog_prompt">请准备好实体遥控器。点击开始后,请将遥控器对准红外控制器并按下目标按键。</string>
|
||||||
|
<string name="infrared_learn_dialog_input_label">按键名称(如:电源)</string>
|
||||||
|
<string name="infrared_learn_dialog_start">开始学习</string>
|
||||||
|
<string name="infrared_rename_dialog_title">重命名按键</string>
|
||||||
|
<string name="infrared_rename_dialog_input_label">新按键名称</string>
|
||||||
|
<string name="infrared_delete_dialog_title">删除按键</string>
|
||||||
|
<string name="infrared_delete_dialog_message">仅从本机列表移除,ABox 服务端可能仍保留该按键。</string>
|
||||||
|
<string name="infrared_dialog_cancel">取消</string>
|
||||||
|
<string name="infrared_dialog_confirm">确认</string>
|
||||||
|
<string name="infrared_back">返回</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="infrared_title">Infrared Control</string>
|
||||||
|
<string name="infrared_mode_fake">Mode: Fake / Simulated</string>
|
||||||
|
<string name="infrared_mode_real">Mode: Real ABSDK</string>
|
||||||
|
<string name="infrared_switch_mode">Switch Mode</string>
|
||||||
|
<string name="infrared_device_name_label">Infrared Device Name</string>
|
||||||
|
<string name="infrared_device_name_hint">e.g. ir-living</string>
|
||||||
|
<string name="infrared_saved_device">Saved Device: %1$s</string>
|
||||||
|
<string name="infrared_no_device_saved">No infrared device configured</string>
|
||||||
|
<string name="infrared_save_device">Save Device</string>
|
||||||
|
<string name="infrared_keys_title">Learned Keys</string>
|
||||||
|
<string name="infrared_no_keys">No infrared keys saved</string>
|
||||||
|
<string name="infrared_learn_new_key">Learn New Key</string>
|
||||||
|
<string name="infrared_status_learning">Learning…</string>
|
||||||
|
<string name="infrared_status_ready">Ready</string>
|
||||||
|
<string name="infrared_status_failed">Failed</string>
|
||||||
|
<string name="infrared_send">Send</string>
|
||||||
|
<string name="infrared_sending">Sending…</string>
|
||||||
|
<string name="infrared_retry">Retry</string>
|
||||||
|
<string name="infrared_rename">Rename</string>
|
||||||
|
<string name="infrared_delete">Delete</string>
|
||||||
|
<string name="infrared_learn_dialog_title">Learn Infrared Key</string>
|
||||||
|
<string name="infrared_learn_dialog_prompt">Prepare the physical remote. After starting, point it at the infrared controller and press the target button.</string>
|
||||||
|
<string name="infrared_learn_dialog_input_label">Key Name (e.g. Power)</string>
|
||||||
|
<string name="infrared_learn_dialog_start">Start Learning</string>
|
||||||
|
<string name="infrared_rename_dialog_title">Rename Key</string>
|
||||||
|
<string name="infrared_rename_dialog_input_label">New Display Name</string>
|
||||||
|
<string name="infrared_delete_dialog_title">Delete Key</string>
|
||||||
|
<string name="infrared_delete_dialog_message">Only removes the key locally from this app. ABox server may retain the learned key.</string>
|
||||||
|
<string name="infrared_dialog_cancel">Cancel</string>
|
||||||
|
<string name="infrared_dialog_confirm">Confirm</string>
|
||||||
|
<string name="infrared_back">Back</string>
|
||||||
|
</resources>
|
||||||
Reference in New Issue
Block a user