feat(scene): add scene editor, executor and fakes (phase 1) #5

Merged
KremeCN merged 10 commits from feat/scene-editor into main 2026-08-25 20:20:19 +08:00
3 changed files with 80 additions and 77 deletions
Showing only changes of commit db6a7978a9 - Show all commits
@@ -1,6 +1,7 @@
package com.flagship.abox.manager.ui.scene package com.flagship.abox.manager.ui.scene
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -41,6 +42,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
@@ -157,10 +159,11 @@ fun SceneEditorScreen(
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
) { ) {
item(key = "title_card") { item(key = "title_card") {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
OutlinedTextField( OutlinedTextField(
@@ -188,10 +191,11 @@ fun SceneEditorScreen(
if (state.type == AutomationType.AUTOMATIC) { if (state.type == AutomationType.AUTOMATIC) {
item(key = "triggers_card") { item(key = "triggers_card") {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row( Row(
@@ -237,10 +241,11 @@ fun SceneEditorScreen(
} }
item(key = "conditions_card") { item(key = "conditions_card") {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row( Row(
@@ -285,10 +290,11 @@ fun SceneEditorScreen(
} }
item(key = "actions_header") { item(key = "actions_header") {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row( Row(
@@ -353,10 +359,11 @@ fun SceneEditorScreen(
} }
item(key = "advanced_options") { item(key = "advanced_options") {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Row( Row(
@@ -416,10 +423,11 @@ fun SceneEditorScreen(
} }
item(key = "other_options") { item(key = "other_options") {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Row( Row(
@@ -637,10 +645,11 @@ private fun ActionCardRow(
onMoveDown: (Int) -> Unit, onMoveDown: (Int) -> Unit,
onRemove: () -> Unit, onRemove: () -> Unit,
) { ) {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -1,5 +1,6 @@
package com.flagship.abox.manager.ui.scene package com.flagship.abox.manager.ui.scene
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -32,6 +33,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -184,16 +186,21 @@ private fun CompletedContent(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
item(key = "summary") { item(key = "summary") {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.outlinedCardColors(
containerColor = if (result.allSucceeded) { containerColor = if (result.allSucceeded) {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
} else { } else {
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f) MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
}, },
), ),
border = BorderStroke(
1.dp,
if (result.allSucceeded) MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)
else MaterialTheme.colorScheme.error.copy(alpha = 0.4f),
),
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text( Text(
@@ -256,10 +263,11 @@ private fun CompletedContent(
@Composable @Composable
private fun ActionResultRow(stepNumber: Int, entry: SceneActionExecutionResult) { private fun ActionResultRow(stepNumber: Int, entry: SceneActionExecutionResult) {
Card( OutlinedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -1,6 +1,7 @@
package com.flagship.abox.manager.ui.scene package com.flagship.abox.manager.ui.scene
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background 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
@@ -17,7 +18,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
@@ -27,7 +27,6 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Schedule import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@@ -37,6 +36,7 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -95,6 +95,7 @@ fun SceneListScreen(
} }
Scaffold( Scaffold(
containerColor = Color.Transparent,
floatingActionButton = { floatingActionButton = {
Box { Box {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
@@ -145,6 +146,8 @@ fun SceneListScreen(
item(key = "tabs") { item(key = "tabs") {
SecondaryTabRow( SecondaryTabRow(
selectedTabIndex = selectedTab.ordinal, selectedTabIndex = selectedTab.ordinal,
containerColor = Color.Transparent,
divider = {},
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(12.dp)), modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(12.dp)),
) { ) {
SceneFilterTab.entries.forEach { tab -> SceneFilterTab.entries.forEach { tab ->
@@ -219,17 +222,20 @@ private fun SceneCard(
onEdit: () -> Unit, onEdit: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
) { ) {
Card( val accentColor = scene.color.toColorOrNull() ?: MaterialTheme.colorScheme.primary
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), OutlinedCard(
colors = CardDefaults.cardColors( modifier = Modifier
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f), .fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.outlinedCardColors(
containerColor = MaterialTheme.colorScheme.surface,
), ),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier.padding(16.dp),
.clickable(onClick = onClick)
.padding(16.dp),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -239,7 +245,7 @@ private fun SceneCard(
modifier = Modifier modifier = Modifier
.size(44.dp) .size(44.dp)
.background( .background(
color = scene.color.toColorOrNull() ?: MaterialTheme.colorScheme.primary, color = accentColor.copy(alpha = 0.12f),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
), ),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -284,40 +290,30 @@ private fun SceneCard(
} }
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(12.dp))
// Trigger & Action summary pills // Trigger & Action summary and controls
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
// Summary text without nested heavy container shells
Row( Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
if (scene.type == AutomationType.AUTOMATIC && scene.triggers.isNotEmpty()) { if (scene.type == AutomationType.AUTOMATIC) {
Surface(
shape = RoundedCornerShape(6.dp),
color = MaterialTheme.colorScheme.tertiaryContainer,
) {
Text( Text(
text = "⚡ ${scene.triggers.size} 触发器", text = if (scene.triggers.isNotEmpty()) "⚡ ${scene.triggers.size} 个触发器 • ${scene.actions.size} 个动作" else "${scene.actions.size} 个动作",
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onTertiaryContainer,
) )
} } else {
}
Surface(
shape = RoundedCornerShape(6.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
) {
Text( Text(
text = stringResource(R.string.scene_action_count, scene.actions.size), text = "${scene.actions.size} 个执行动作",
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onSecondaryContainer,
) )
} }
} }
@@ -326,8 +322,9 @@ private fun SceneCard(
if (scene.type == AutomationType.MANUAL) { if (scene.type == AutomationType.MANUAL) {
FilledTonalButton( FilledTonalButton(
onClick = onQuickExecute, onClick = onQuickExecute,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp), contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
modifier = Modifier.height(32.dp), modifier = Modifier.height(32.dp),
shape = RoundedCornerShape(8.dp),
) { ) {
Icon(Icons.Filled.PlayArrow, contentDescription = null, modifier = Modifier.size(16.dp)) Icon(Icons.Filled.PlayArrow, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
@@ -336,10 +333,10 @@ private fun SceneCard(
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
IconButton(onClick = onEdit, modifier = Modifier.size(32.dp)) { IconButton(onClick = onEdit, modifier = Modifier.size(32.dp)) {
Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.scene_edit), modifier = Modifier.size(18.dp)) Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.scene_edit), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
} }
IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) { IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) {
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.scene_delete), modifier = Modifier.size(18.dp)) Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.scene_delete), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
} }
} }
} }
@@ -349,27 +346,20 @@ private fun SceneCard(
@Composable @Composable
private fun TypeBadge(type: AutomationType) { private fun TypeBadge(type: AutomationType) {
val (label, bg, fg) = when (type) { val label = when (type) {
AutomationType.MANUAL -> Triple( AutomationType.MANUAL -> stringResource(R.string.scene_tab_manual)
stringResource(R.string.scene_tab_manual), AutomationType.AUTOMATIC -> stringResource(R.string.scene_tab_automatic)
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.onPrimaryContainer,
)
AutomationType.AUTOMATIC -> Triple(
stringResource(R.string.scene_tab_automatic),
MaterialTheme.colorScheme.secondaryContainer,
MaterialTheme.colorScheme.onSecondaryContainer,
)
} }
Surface( Surface(
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
color = bg, color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)),
) { ) {
Text( Text(
text = label, text = label,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = fg, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
} }
@@ -377,9 +367,7 @@ private fun TypeBadge(type: AutomationType) {
@Composable @Composable
private fun EmptySceneList(modifier: Modifier = Modifier) { private fun EmptySceneList(modifier: Modifier = Modifier) {
Column( Column(
modifier = modifier modifier = modifier,
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
@@ -423,5 +411,3 @@ internal fun SceneColor.toColorOrNull(): Color? {
val cleanHex = hex.removePrefix("#") val cleanHex = hex.removePrefix("#")
return runCatching { Color(cleanHex.toLong(16) or 0xFF000000.toInt().toLong()) }.getOrNull() return runCatching { Color(cleanHex.toLong(16) or 0xFF000000.toInt().toLong()) }.getOrNull()
} }