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