feat(ui): complete ABox app experience redesign #10

Merged
KremeCN merged 40 commits from feat/ui-ux-redesign into main 2026-08-27 23:25:08 +08:00
2 changed files with 63 additions and 5 deletions
Showing only changes of commit 713106df14 - Show all commits
@@ -8,6 +8,9 @@ import com.flagship.abox.manager.domain.Scene
import com.flagship.abox.manager.domain.SceneExecutionResult import com.flagship.abox.manager.domain.SceneExecutionResult
import com.flagship.abox.manager.domain.SceneExecutor import com.flagship.abox.manager.domain.SceneExecutor
import com.flagship.abox.manager.domain.SceneId import com.flagship.abox.manager.domain.SceneId
import com.flagship.abox.manager.domain.SceneExecutionId
import com.flagship.abox.manager.domain.SceneExecutionSource
import com.flagship.abox.manager.domain.SceneExecutionUpdate
import com.flagship.abox.manager.domain.SceneRepository import com.flagship.abox.manager.domain.SceneRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@@ -15,6 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
sealed interface SceneExecutionUiState { sealed interface SceneExecutionUiState {
data object Loading : SceneExecutionUiState data object Loading : SceneExecutionUiState
@@ -71,11 +75,19 @@ class SceneExecutionViewModel @Inject constructor(
_uiState.value = SceneExecutionUiState.Failed("Active profile mismatch") _uiState.value = SceneExecutionUiState.Failed("Active profile mismatch")
return@launch return@launch
} }
when (val result = sceneExecutor.execute(scene)) { sceneExecutor.execute(
is DomainResult.Success -> _uiState.value = SceneExecutionUiState.Completed(result.value) scene = scene,
is DomainResult.Failure -> _uiState.value = SceneExecutionUiState.Failed( executionId = SceneExecutionId("manual-${scene.id.value}"),
result.error.detail ?: "Execution failed", source = SceneExecutionSource.USER,
).collect { update ->
when (update) {
is SceneExecutionUpdate.Completed -> _uiState.value = SceneExecutionUiState.Completed(update.result)
is SceneExecutionUpdate.Rejected -> _uiState.value = SceneExecutionUiState.Failed(
update.error.detail ?: "Execution failed",
) )
is SceneExecutionUpdate.Started -> _uiState.value = SceneExecutionUiState.Running(update.scene)
else -> Unit
}
} }
} }
} }
@@ -9,6 +9,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/** /**
* Production implementation of [SceneExecutor]. * Production implementation of [SceneExecutor].
@@ -24,6 +26,50 @@ class DefaultSceneExecutor(
) : SceneExecutor { ) : SceneExecutor {
private val runningScenes = ConcurrentHashMap<SceneId, Boolean>() private val runningScenes = ConcurrentHashMap<SceneId, Boolean>()
override fun execute(
scene: Scene,
executionId: SceneExecutionId,
source: SceneExecutionSource,
): Flow<SceneExecutionUpdate> = flow {
val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null
if (alreadyRunning) {
emit(SceneExecutionUpdate.Rejected(DomainError.OperationFailed(detail = "Scene is already running")))
return@flow
}
val startTime = System.currentTimeMillis()
try {
emit(SceneExecutionUpdate.Started(executionId, scene))
val results = mutableListOf<SceneActionExecutionResult>()
var aborted = false
for (action in scene.actions) {
if (aborted) {
val skipped = SceneActionExecutionResult(action, SceneActionOutcome.SKIPPED, null)
results += skipped
emit(SceneExecutionUpdate.StepFinished(skipped))
continue
}
emit(SceneExecutionUpdate.StepPending(action))
emit(SceneExecutionUpdate.StepRunning(action))
val started = System.currentTimeMillis()
val result = executeAction(scene.profileId, action).copy(
executionDurationMillis = System.currentTimeMillis() - started,
)
results += result
emit(SceneExecutionUpdate.StepFinished(result))
if (result.outcome == SceneActionOutcome.FAILURE && scene.strategy == ExecutionStrategy.ABORT_ON_ERROR) {
aborted = true
}
}
emit(
SceneExecutionUpdate.Completed(
SceneExecutionResult(scene, results, System.currentTimeMillis() - startTime),
),
)
} finally {
runningScenes.remove(scene.id)
}
}
override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> { override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> {
val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null
if (alreadyRunning) { if (alreadyRunning) {