Refactor to MVI pattern with sealed AdbCommanderAction

Replace individual callback parameters with a single
onAction: (AdbCommanderAction) -> Unit across all composables.
ViewModel dispatches actions via onAction(action) method.
This commit is contained in:
Adit Lal 2026-02-18 22:00:27 +05:30
parent df19d3c90c
commit 6bbbd6568c
7 changed files with 213 additions and 175 deletions

View file

@ -20,6 +20,7 @@ import io.github.openflocon.domain.adbcommander.usecase.UpdateSavedCommandUseCas
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.adbcommander.mapper.toUiModel
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderAction
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderTab
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderUiState
import io.github.openflocon.flocondesktop.features.adbcommander.model.ConsoleOutputEntry
@ -83,15 +84,43 @@ class AdbCommanderViewModel(
initialValue = AdbCommanderUiState(),
)
fun onTabSelected(tab: AdbCommanderTab) {
fun onAction(action: AdbCommanderAction) {
when (action) {
is AdbCommanderAction.TabSelected -> onTabSelected(action.tab)
is AdbCommanderAction.CommandInputChanged -> onCommandInputChanged(action.input)
is AdbCommanderAction.ExecuteCommand -> onExecuteCommand()
is AdbCommanderAction.SaveCurrentCommand -> onSaveCurrentCommand()
is AdbCommanderAction.RunSavedCommand -> onRunSavedCommand(action.command)
is AdbCommanderAction.DeleteSavedCommand -> onDeleteSavedCommand(action.id)
is AdbCommanderAction.SaveQuickCommand -> onSaveQuickCommand(action.name, action.command)
is AdbCommanderAction.ClearHistory -> onClearHistory()
is AdbCommanderAction.RerunCommand -> onRerunCommand(action.command)
is AdbCommanderAction.ClearConsole -> onClearConsole()
is AdbCommanderAction.ShowFlowEditor -> onShowFlowEditor(action.flowId)
is AdbCommanderAction.DismissFlowEditor -> onDismissFlowEditor()
is AdbCommanderAction.FlowEditorNameChanged -> onFlowEditorNameChanged(action.name)
is AdbCommanderAction.FlowEditorDescriptionChanged -> onFlowEditorDescriptionChanged(action.description)
is AdbCommanderAction.FlowEditorStepCommandChanged -> onFlowEditorStepCommandChanged(action.index, action.command)
is AdbCommanderAction.FlowEditorStepLabelChanged -> onFlowEditorStepLabelChanged(action.index, action.label)
is AdbCommanderAction.FlowEditorStepDelayChanged -> onFlowEditorStepDelayChanged(action.index, action.delay)
is AdbCommanderAction.FlowEditorAddStep -> onFlowEditorAddStep()
is AdbCommanderAction.FlowEditorRemoveStep -> onFlowEditorRemoveStep(action.index)
is AdbCommanderAction.SaveFlow -> onSaveFlow()
is AdbCommanderAction.DeleteFlow -> onDeleteFlow(action.id)
is AdbCommanderAction.ExecuteFlow -> onExecuteFlow(action.flowId)
is AdbCommanderAction.CancelFlowExecution -> onCancelFlowExecution()
}
}
private fun onTabSelected(tab: AdbCommanderTab) {
localState.update { it.copy(selectedTab = tab) }
}
fun onCommandInputChanged(input: String) {
private fun onCommandInputChanged(input: String) {
localState.update { it.copy(commandInput = input) }
}
fun onExecuteCommand() {
private fun onExecuteCommand() {
val command = localState.value.commandInput.trim()
if (command.isEmpty()) return
@ -130,12 +159,12 @@ class AdbCommanderViewModel(
}
}
fun onRunSavedCommand(command: String) {
private fun onRunSavedCommand(command: String) {
localState.update { it.copy(commandInput = command) }
onExecuteCommand()
}
fun onSaveCurrentCommand() {
private fun onSaveCurrentCommand() {
val command = localState.value.commandInput.trim()
if (command.isEmpty()) {
feedbackDisplayer.displayMessage(
@ -157,7 +186,7 @@ class AdbCommanderViewModel(
}
}
fun onSaveQuickCommand(name: String, command: String) {
private fun onSaveQuickCommand(name: String, command: String) {
viewModelScope.launch(dispatcherProvider.viewModel) {
saveCommandUseCase(
AdbCommandDomainModel(
@ -171,27 +200,26 @@ class AdbCommanderViewModel(
}
}
fun onDeleteSavedCommand(id: Long) {
private fun onDeleteSavedCommand(id: Long) {
viewModelScope.launch(dispatcherProvider.viewModel) {
deleteSavedCommandUseCase(id)
feedbackDisplayer.displayMessage("Command deleted")
}
}
fun onClearHistory() {
private fun onClearHistory() {
viewModelScope.launch(dispatcherProvider.viewModel) {
clearCommandHistoryUseCase()
feedbackDisplayer.displayMessage("History cleared")
}
}
fun onRerunCommand(command: String) {
private fun onRerunCommand(command: String) {
localState.update { it.copy(commandInput = command) }
onExecuteCommand()
}
// Flow editor
fun onShowFlowEditor(flowId: Long? = null) {
private fun onShowFlowEditor(flowId: Long? = null) {
if (flowId != null) {
val flow = domainFlows.value.find { it.id == flowId }
localState.update {
@ -221,23 +249,23 @@ class AdbCommanderViewModel(
}
}
fun onDismissFlowEditor() {
private fun onDismissFlowEditor() {
localState.update { it.copy(showFlowEditor = false) }
}
fun onFlowEditorNameChanged(name: String) {
private fun onFlowEditorNameChanged(name: String) {
localState.update {
it.copy(flowEditorState = it.flowEditorState.copy(name = name))
}
}
fun onFlowEditorDescriptionChanged(description: String) {
private fun onFlowEditorDescriptionChanged(description: String) {
localState.update {
it.copy(flowEditorState = it.flowEditorState.copy(description = description))
}
}
fun onFlowEditorStepCommandChanged(index: Int, command: String) {
private fun onFlowEditorStepCommandChanged(index: Int, command: String) {
localState.update {
val steps = it.flowEditorState.steps.toMutableList()
if (index < steps.size) {
@ -247,7 +275,7 @@ class AdbCommanderViewModel(
}
}
fun onFlowEditorStepLabelChanged(index: Int, label: String) {
private fun onFlowEditorStepLabelChanged(index: Int, label: String) {
localState.update {
val steps = it.flowEditorState.steps.toMutableList()
if (index < steps.size) {
@ -257,7 +285,7 @@ class AdbCommanderViewModel(
}
}
fun onFlowEditorStepDelayChanged(index: Int, delay: String) {
private fun onFlowEditorStepDelayChanged(index: Int, delay: String) {
localState.update {
val steps = it.flowEditorState.steps.toMutableList()
if (index < steps.size) {
@ -267,7 +295,7 @@ class AdbCommanderViewModel(
}
}
fun onFlowEditorAddStep() {
private fun onFlowEditorAddStep() {
localState.update {
it.copy(
flowEditorState = it.flowEditorState.copy(
@ -277,7 +305,7 @@ class AdbCommanderViewModel(
}
}
fun onFlowEditorRemoveStep(index: Int) {
private fun onFlowEditorRemoveStep(index: Int) {
localState.update {
val steps = it.flowEditorState.steps.toMutableList()
if (steps.size > 1 && index < steps.size) {
@ -287,7 +315,7 @@ class AdbCommanderViewModel(
}
}
fun onSaveFlow() {
private fun onSaveFlow() {
val editor = localState.value.flowEditorState
if (editor.name.isBlank()) {
feedbackDisplayer.displayMessage(
@ -329,14 +357,14 @@ class AdbCommanderViewModel(
}
}
fun onDeleteFlow(id: Long) {
private fun onDeleteFlow(id: Long) {
viewModelScope.launch(dispatcherProvider.viewModel) {
deleteFlowUseCase(id)
feedbackDisplayer.displayMessage("Flow deleted")
}
}
fun onExecuteFlow(flowId: Long) {
private fun onExecuteFlow(flowId: Long) {
val flow = domainFlows.value.find { it.id == flowId } ?: return
flowExecutionJob?.cancel()
@ -349,12 +377,12 @@ class AdbCommanderViewModel(
}
}
fun onCancelFlowExecution() {
private fun onCancelFlowExecution() {
flowExecutionJob?.cancel()
flowExecutionJob = null
}
fun onClearConsole() {
private fun onClearConsole() {
localState.update { it.copy(consoleOutput = emptyList(), flowExecution = null) }
}
}

View file

@ -0,0 +1,29 @@
package io.github.openflocon.flocondesktop.features.adbcommander.model
sealed interface AdbCommanderAction {
data class TabSelected(val tab: AdbCommanderTab) : AdbCommanderAction
data class CommandInputChanged(val input: String) : AdbCommanderAction
data object ExecuteCommand : AdbCommanderAction
data object SaveCurrentCommand : AdbCommanderAction
data class RunSavedCommand(val command: String) : AdbCommanderAction
data class DeleteSavedCommand(val id: Long) : AdbCommanderAction
data class SaveQuickCommand(val name: String, val command: String) : AdbCommanderAction
data object ClearHistory : AdbCommanderAction
data class RerunCommand(val command: String) : AdbCommanderAction
data object ClearConsole : AdbCommanderAction
// Flow actions
data class ShowFlowEditor(val flowId: Long? = null) : AdbCommanderAction
data object DismissFlowEditor : AdbCommanderAction
data class FlowEditorNameChanged(val name: String) : AdbCommanderAction
data class FlowEditorDescriptionChanged(val description: String) : AdbCommanderAction
data class FlowEditorStepCommandChanged(val index: Int, val command: String) : AdbCommanderAction
data class FlowEditorStepLabelChanged(val index: Int, val label: String) : AdbCommanderAction
data class FlowEditorStepDelayChanged(val index: Int, val delay: String) : AdbCommanderAction
data object FlowEditorAddStep : AdbCommanderAction
data class FlowEditorRemoveStep(val index: Int) : AdbCommanderAction
data object SaveFlow : AdbCommanderAction
data class DeleteFlow(val id: Long) : AdbCommanderAction
data class ExecuteFlow(val flowId: Long) : AdbCommanderAction
data object CancelFlowExecution : AdbCommanderAction
}

View file

@ -1,9 +1,7 @@
package io.github.openflocon.flocondesktop.features.adbcommander.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
@ -17,9 +15,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.github.openflocon.flocondesktop.features.adbcommander.AdbCommanderViewModel
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderAction
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderTab
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderUiState
import io.github.openflocon.library.designsystem.FloconTheme
@ -30,7 +28,10 @@ import io.github.openflocon.library.designsystem.components.FloconIconButton
import io.github.openflocon.library.designsystem.components.FloconPageTopBar
import io.github.openflocon.library.designsystem.components.FloconTextField
import io.github.openflocon.library.designsystem.components.defaultPlaceHolder
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.adb_commander_input_placeholder
@Composable
fun AdbCommanderScreen(modifier: Modifier = Modifier) {
@ -39,29 +40,7 @@ fun AdbCommanderScreen(modifier: Modifier = Modifier) {
AdbCommanderScreen(
uiState = uiState,
onTabSelected = viewModel::onTabSelected,
onCommandInputChanged = viewModel::onCommandInputChanged,
onExecuteCommand = viewModel::onExecuteCommand,
onSaveCurrentCommand = viewModel::onSaveCurrentCommand,
onRunSavedCommand = viewModel::onRunSavedCommand,
onDeleteSavedCommand = viewModel::onDeleteSavedCommand,
onSaveQuickCommand = viewModel::onSaveQuickCommand,
onClearHistory = viewModel::onClearHistory,
onRerunCommand = viewModel::onRerunCommand,
onClearConsole = viewModel::onClearConsole,
onShowFlowEditor = viewModel::onShowFlowEditor,
onDismissFlowEditor = viewModel::onDismissFlowEditor,
onFlowEditorNameChanged = viewModel::onFlowEditorNameChanged,
onFlowEditorDescriptionChanged = viewModel::onFlowEditorDescriptionChanged,
onFlowEditorStepCommandChanged = viewModel::onFlowEditorStepCommandChanged,
onFlowEditorStepLabelChanged = viewModel::onFlowEditorStepLabelChanged,
onFlowEditorStepDelayChanged = viewModel::onFlowEditorStepDelayChanged,
onFlowEditorAddStep = viewModel::onFlowEditorAddStep,
onFlowEditorRemoveStep = viewModel::onFlowEditorRemoveStep,
onSaveFlow = viewModel::onSaveFlow,
onDeleteFlow = viewModel::onDeleteFlow,
onExecuteFlow = viewModel::onExecuteFlow,
onCancelFlowExecution = viewModel::onCancelFlowExecution,
onAction = viewModel::onAction,
modifier = modifier,
)
}
@ -69,29 +48,7 @@ fun AdbCommanderScreen(modifier: Modifier = Modifier) {
@Composable
private fun AdbCommanderScreen(
uiState: AdbCommanderUiState,
onTabSelected: (AdbCommanderTab) -> Unit,
onCommandInputChanged: (String) -> Unit,
onExecuteCommand: () -> Unit,
onSaveCurrentCommand: () -> Unit,
onRunSavedCommand: (String) -> Unit,
onDeleteSavedCommand: (Long) -> Unit,
onSaveQuickCommand: (String, String) -> Unit,
onClearHistory: () -> Unit,
onRerunCommand: (String) -> Unit,
onClearConsole: () -> Unit,
onShowFlowEditor: (Long?) -> Unit,
onDismissFlowEditor: () -> Unit,
onFlowEditorNameChanged: (String) -> Unit,
onFlowEditorDescriptionChanged: (String) -> Unit,
onFlowEditorStepCommandChanged: (Int, String) -> Unit,
onFlowEditorStepLabelChanged: (Int, String) -> Unit,
onFlowEditorStepDelayChanged: (Int, String) -> Unit,
onFlowEditorAddStep: () -> Unit,
onFlowEditorRemoveStep: (Int) -> Unit,
onSaveFlow: () -> Unit,
onDeleteFlow: (Long) -> Unit,
onExecuteFlow: (Long) -> Unit,
onCancelFlowExecution: () -> Unit,
onAction: (AdbCommanderAction) -> Unit,
modifier: Modifier = Modifier,
) {
FloconFeature(modifier = modifier) {
@ -100,7 +57,7 @@ private fun AdbCommanderScreen(
selector = {
AdbCommanderTab.entries.forEach { tab ->
FloconButton(
onClick = { onTabSelected(tab) },
onClick = { onAction(AdbCommanderAction.TabSelected(tab)) },
containerColor = if (tab == uiState.selectedTab) {
FloconTheme.colorPalette.accent
} else {
@ -114,19 +71,19 @@ private fun AdbCommanderScreen(
filterBar = {
FloconTextField(
value = uiState.commandInput,
onValueChange = onCommandInputChanged,
placeholder = defaultPlaceHolder("Enter ADB command (e.g. shell echo hello)"),
onValueChange = { onAction(AdbCommanderAction.CommandInputChanged(it)) },
placeholder = defaultPlaceHolder(stringResource(Res.string.adb_commander_input_placeholder)),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
keyboardActions = KeyboardActions(onSend = { onExecuteCommand() }),
keyboardActions = KeyboardActions(onSend = { onAction(AdbCommanderAction.ExecuteCommand) }),
modifier = Modifier.weight(1f),
)
},
actions = {
FloconIconButton(onClick = onSaveCurrentCommand) {
FloconIconButton(onClick = { onAction(AdbCommanderAction.SaveCurrentCommand) }) {
FloconIcon(imageVector = Icons.Outlined.Save)
}
FloconIconButton(onClick = onExecuteCommand) {
FloconIconButton(onClick = { onAction(AdbCommanderAction.ExecuteCommand) }) {
FloconIcon(imageVector = Icons.AutoMirrored.Outlined.Send)
}
},
@ -143,25 +100,18 @@ private fun AdbCommanderScreen(
consoleOutput = uiState.consoleOutput,
flowExecution = uiState.flowExecution,
isExecuting = uiState.isExecuting,
onClearConsole = onClearConsole,
onCancelFlowExecution = onCancelFlowExecution,
onAction = onAction,
modifier = Modifier.fillMaxSize(),
)
AdbCommanderTab.Library -> LibraryContent(
savedCommands = uiState.savedCommands,
flows = uiState.flows,
onRunSavedCommand = onRunSavedCommand,
onDeleteSavedCommand = onDeleteSavedCommand,
onShowFlowEditor = onShowFlowEditor,
onDeleteFlow = onDeleteFlow,
onExecuteFlow = onExecuteFlow,
onSaveQuickCommand = onSaveQuickCommand,
onAction = onAction,
modifier = Modifier.fillMaxSize(),
)
AdbCommanderTab.History -> HistoryContent(
history = uiState.history,
onClearHistory = onClearHistory,
onRerunCommand = onRerunCommand,
onAction = onAction,
modifier = Modifier.fillMaxSize(),
)
}
@ -171,15 +121,7 @@ private fun AdbCommanderScreen(
if (uiState.showFlowEditor) {
FlowEditorDialog(
state = uiState.flowEditorState,
onDismiss = onDismissFlowEditor,
onNameChanged = onFlowEditorNameChanged,
onDescriptionChanged = onFlowEditorDescriptionChanged,
onStepCommandChanged = onFlowEditorStepCommandChanged,
onStepLabelChanged = onFlowEditorStepLabelChanged,
onStepDelayChanged = onFlowEditorStepDelayChanged,
onAddStep = onFlowEditorAddStep,
onRemoveStep = onFlowEditorRemoveStep,
onSave = onSaveFlow,
onAction = onAction,
)
}
}

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderAction
import io.github.openflocon.flocondesktop.features.adbcommander.model.FlowEditorState
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.FloconButton
@ -25,24 +26,31 @@ import io.github.openflocon.library.designsystem.components.FloconIcon
import io.github.openflocon.library.designsystem.components.FloconIconButton
import io.github.openflocon.library.designsystem.components.FloconTextField
import io.github.openflocon.library.designsystem.components.defaultPlaceHolder
import org.jetbrains.compose.resources.stringResource
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.adb_commander_edit_flow
import flocondesktop.composeapp.generated.resources.adb_commander_new_flow
import flocondesktop.composeapp.generated.resources.adb_commander_flow_name
import flocondesktop.composeapp.generated.resources.adb_commander_flow_description
import flocondesktop.composeapp.generated.resources.adb_commander_steps
import flocondesktop.composeapp.generated.resources.adb_commander_add_step
import flocondesktop.composeapp.generated.resources.adb_commander_step_command
import flocondesktop.composeapp.generated.resources.adb_commander_step_label
import flocondesktop.composeapp.generated.resources.adb_commander_step_delay
@Composable
fun FlowEditorDialog(
state: FlowEditorState,
onDismiss: () -> Unit,
onNameChanged: (String) -> Unit,
onDescriptionChanged: (String) -> Unit,
onStepCommandChanged: (Int, String) -> Unit,
onStepLabelChanged: (Int, String) -> Unit,
onStepDelayChanged: (Int, String) -> Unit,
onAddStep: () -> Unit,
onRemoveStep: (Int) -> Unit,
onSave: () -> Unit,
onAction: (AdbCommanderAction) -> Unit,
) {
FloconDialog(onDismissRequest = onDismiss) {
FloconDialog(onDismissRequest = { onAction(AdbCommanderAction.DismissFlowEditor) }) {
Column {
FloconDialogHeader(
title = if (state.flowId != null) "Edit Flow" else "New Flow",
title = if (state.flowId != null) {
stringResource(Res.string.adb_commander_edit_flow)
} else {
stringResource(Res.string.adb_commander_new_flow)
},
modifier = Modifier.fillMaxWidth(),
)
@ -54,15 +62,15 @@ fun FlowEditorDialog(
) {
FloconTextField(
value = state.name,
onValueChange = onNameChanged,
placeholder = defaultPlaceHolder("Flow name"),
onValueChange = { onAction(AdbCommanderAction.FlowEditorNameChanged(it)) },
placeholder = defaultPlaceHolder(stringResource(Res.string.adb_commander_flow_name)),
modifier = Modifier.fillMaxWidth(),
)
FloconTextField(
value = state.description,
onValueChange = onDescriptionChanged,
placeholder = defaultPlaceHolder("Description (optional)"),
onValueChange = { onAction(AdbCommanderAction.FlowEditorDescriptionChanged(it)) },
placeholder = defaultPlaceHolder(stringResource(Res.string.adb_commander_flow_description)),
modifier = Modifier.fillMaxWidth(),
)
@ -72,13 +80,13 @@ fun FlowEditorDialog(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Steps",
text = stringResource(Res.string.adb_commander_steps),
style = FloconTheme.typography.titleSmall,
color = FloconTheme.colorPalette.onPrimary,
)
FloconButton(onClick = onAddStep) {
FloconButton(onClick = { onAction(AdbCommanderAction.FlowEditorAddStep) }) {
FloconIcon(imageVector = Icons.Outlined.Add)
Text("Add Step")
Text(stringResource(Res.string.adb_commander_add_step))
}
}
@ -91,10 +99,10 @@ fun FlowEditorDialog(
command = step.command,
label = step.label,
delayAfterMs = step.delayAfterMs,
onCommandChanged = { onStepCommandChanged(index, it) },
onLabelChanged = { onStepLabelChanged(index, it) },
onDelayChanged = { onStepDelayChanged(index, it) },
onRemove = { onRemoveStep(index) },
onCommandChanged = { onAction(AdbCommanderAction.FlowEditorStepCommandChanged(index, it)) },
onLabelChanged = { onAction(AdbCommanderAction.FlowEditorStepLabelChanged(index, it)) },
onDelayChanged = { onAction(AdbCommanderAction.FlowEditorStepDelayChanged(index, it)) },
onRemove = { onAction(AdbCommanderAction.FlowEditorRemoveStep(index)) },
canRemove = state.steps.size > 1,
modifier = Modifier.fillMaxWidth(),
)
@ -102,8 +110,8 @@ fun FlowEditorDialog(
}
FloconDialogButtons(
onCancel = onDismiss,
onValidate = onSave,
onCancel = { onAction(AdbCommanderAction.DismissFlowEditor) },
onValidate = { onAction(AdbCommanderAction.SaveFlow) },
modifier = Modifier.padding(top = 8.dp),
)
}
@ -142,20 +150,20 @@ private fun StepEditorItem(
FloconTextField(
value = command,
onValueChange = onCommandChanged,
placeholder = defaultPlaceHolder("ADB command"),
placeholder = defaultPlaceHolder(stringResource(Res.string.adb_commander_step_command)),
modifier = Modifier.fillMaxWidth(),
)
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
FloconTextField(
value = label,
onValueChange = onLabelChanged,
placeholder = defaultPlaceHolder("Label (optional)"),
placeholder = defaultPlaceHolder(stringResource(Res.string.adb_commander_step_label)),
modifier = Modifier.weight(1f),
)
FloconTextField(
value = delayAfterMs,
onValueChange = onDelayChanged,
placeholder = defaultPlaceHolder("Delay (ms)"),
placeholder = defaultPlaceHolder(stringResource(Res.string.adb_commander_step_delay)),
modifier = Modifier.weight(0.5f),
)
}

View file

@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material.icons.outlined.Replay
import androidx.compose.material3.Text
@ -23,17 +25,21 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderAction
import io.github.openflocon.flocondesktop.features.adbcommander.model.HistoryEntryUiModel
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.FloconIcon
import io.github.openflocon.library.designsystem.components.FloconIconButton
import io.github.openflocon.library.designsystem.components.FloconTextButton
import org.jetbrains.compose.resources.stringResource
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.adb_commander_clear_all
import flocondesktop.composeapp.generated.resources.adb_commander_no_history
@Composable
fun HistoryContent(
history: List<HistoryEntryUiModel>,
onClearHistory: () -> Unit,
onRerunCommand: (String) -> Unit,
onAction: (AdbCommanderAction) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@ -41,15 +47,15 @@ fun HistoryContent(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.End,
) {
FloconTextButton(onClick = onClearHistory) {
FloconTextButton(onClick = { onAction(AdbCommanderAction.ClearHistory) }) {
FloconIcon(imageVector = Icons.Outlined.DeleteSweep)
Text("Clear All")
Text(stringResource(Res.string.adb_commander_clear_all))
}
}
if (history.isEmpty()) {
Text(
text = "No command history",
text = stringResource(Res.string.adb_commander_no_history),
style = FloconTheme.typography.bodySmall,
color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.6f),
modifier = Modifier.padding(16.dp),
@ -64,7 +70,7 @@ fun HistoryContent(
items(history) { entry ->
HistoryEntryView(
entry = entry,
onRerun = { onRerunCommand(entry.command) },
onRerun = { onAction(AdbCommanderAction.RerunCommand(entry.command)) },
modifier = Modifier.fillMaxWidth(),
)
}
@ -90,10 +96,9 @@ private fun HistoryEntryView(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = if (entry.isSuccess) "+" else "x",
style = FloconTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
color = if (entry.isSuccess) {
FloconIcon(
imageVector = if (entry.isSuccess) Icons.Outlined.Check else Icons.Outlined.Close,
tint = if (entry.isSuccess) {
FloconTheme.colorPalette.onPrimary
} else {
FloconTheme.colorPalette.error

View file

@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderAction
import io.github.openflocon.flocondesktop.features.adbcommander.model.FlowUiModel
import io.github.openflocon.flocondesktop.features.adbcommander.model.QuickCommand
import io.github.openflocon.flocondesktop.features.adbcommander.model.SavedCommandUiModel
@ -27,17 +28,20 @@ import io.github.openflocon.library.designsystem.components.FloconButton
import io.github.openflocon.library.designsystem.components.FloconIcon
import io.github.openflocon.library.designsystem.components.FloconIconButton
import io.github.openflocon.library.designsystem.components.FloconSection
import org.jetbrains.compose.resources.stringResource
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.adb_commander_saved_commands
import flocondesktop.composeapp.generated.resources.adb_commander_no_saved_commands
import flocondesktop.composeapp.generated.resources.adb_commander_automation_flows
import flocondesktop.composeapp.generated.resources.adb_commander_new_flow
import flocondesktop.composeapp.generated.resources.adb_commander_no_flows
import flocondesktop.composeapp.generated.resources.adb_commander_steps_count
@Composable
fun LibraryContent(
savedCommands: List<SavedCommandUiModel>,
flows: List<FlowUiModel>,
onRunSavedCommand: (String) -> Unit,
onDeleteSavedCommand: (Long) -> Unit,
onShowFlowEditor: (Long?) -> Unit,
onDeleteFlow: (Long) -> Unit,
onExecuteFlow: (Long) -> Unit,
onSaveQuickCommand: (String, String) -> Unit,
onAction: (AdbCommanderAction) -> Unit,
modifier: Modifier = Modifier,
) {
val categories = defaultQuickCommands.groupBy { it.category }
@ -49,13 +53,13 @@ fun LibraryContent(
) {
item {
FloconSection(
title = "Saved Commands",
title = stringResource(Res.string.adb_commander_saved_commands),
initialValue = true,
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (savedCommands.isEmpty()) {
Text(
text = "No saved commands",
text = stringResource(Res.string.adb_commander_no_saved_commands),
style = FloconTheme.typography.bodySmall,
color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.6f),
modifier = Modifier.padding(8.dp),
@ -64,8 +68,8 @@ fun LibraryContent(
savedCommands.forEach { command ->
SavedCommandItem(
command = command,
onRun = { onRunSavedCommand(command.command) },
onDelete = { onDeleteSavedCommand(command.id) },
onRun = { onAction(AdbCommanderAction.RunSavedCommand(command.command)) },
onDelete = { onAction(AdbCommanderAction.DeleteSavedCommand(command.id)) },
modifier = Modifier.fillMaxWidth(),
)
}
@ -74,19 +78,19 @@ fun LibraryContent(
}
item {
FloconSection(
title = "Automation Flows",
title = stringResource(Res.string.adb_commander_automation_flows),
initialValue = true,
actions = {
FloconButton(onClick = { onShowFlowEditor(null) }) {
FloconButton(onClick = { onAction(AdbCommanderAction.ShowFlowEditor(null)) }) {
FloconIcon(imageVector = Icons.Outlined.Add)
Text("New Flow")
Text(stringResource(Res.string.adb_commander_new_flow))
}
},
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (flows.isEmpty()) {
Text(
text = "No automation flows",
text = stringResource(Res.string.adb_commander_no_flows),
style = FloconTheme.typography.bodySmall,
color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.6f),
modifier = Modifier.padding(8.dp),
@ -95,9 +99,9 @@ fun LibraryContent(
flows.forEach { flow ->
FlowItem(
flow = flow,
onRun = { onExecuteFlow(flow.id) },
onEdit = { onShowFlowEditor(flow.id) },
onDelete = { onDeleteFlow(flow.id) },
onRun = { onAction(AdbCommanderAction.ExecuteFlow(flow.id)) },
onEdit = { onAction(AdbCommanderAction.ShowFlowEditor(flow.id)) },
onDelete = { onAction(AdbCommanderAction.DeleteFlow(flow.id)) },
modifier = Modifier.fillMaxWidth(),
)
}
@ -114,8 +118,8 @@ fun LibraryContent(
commands.forEach { cmd ->
QuickCommandItem(
command = cmd,
onRun = { onRunSavedCommand(cmd.command) },
onSave = { onSaveQuickCommand(cmd.name, cmd.command) },
onRun = { onAction(AdbCommanderAction.RunSavedCommand(cmd.command)) },
onSave = { onAction(AdbCommanderAction.SaveQuickCommand(cmd.name, cmd.command)) },
modifier = Modifier.fillMaxWidth(),
)
}
@ -219,7 +223,7 @@ private fun FlowItem(
color = FloconTheme.colorPalette.onPrimary,
)
Text(
text = "${flow.stepsCount} steps",
text = stringResource(Res.string.adb_commander_steps_count, flow.stepsCount),
style = FloconTheme.typography.bodySmall,
color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.7f),
)

View file

@ -6,22 +6,35 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material.icons.outlined.HourglassEmpty
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material.icons.outlined.RemoveCircle
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import io.github.openflocon.flocondesktop.features.adbcommander.model.AdbCommanderAction
import io.github.openflocon.flocondesktop.features.adbcommander.model.ConsoleOutputEntry
import io.github.openflocon.flocondesktop.features.adbcommander.model.FlowExecutionUiModel
import io.github.openflocon.library.designsystem.FloconTheme
import org.jetbrains.compose.resources.stringResource
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.adb_commander_cancel_flow
import io.github.openflocon.library.designsystem.components.FloconCircularProgressIndicator
import io.github.openflocon.library.designsystem.components.FloconIcon
import io.github.openflocon.library.designsystem.components.FloconIconButton
@ -32,8 +45,7 @@ fun RunnerContent(
consoleOutput: List<ConsoleOutputEntry>,
flowExecution: FlowExecutionUiModel?,
isExecuting: Boolean,
onClearConsole: () -> Unit,
onCancelFlowExecution: () -> Unit,
onAction: (AdbCommanderAction) -> Unit,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
@ -54,12 +66,12 @@ fun RunnerContent(
FloconCircularProgressIndicator()
}
if (flowExecution?.isRunning == true) {
FloconTextButton(onClick = onCancelFlowExecution) {
FloconTextButton(onClick = { onAction(AdbCommanderAction.CancelFlowExecution) }) {
FloconIcon(imageVector = Icons.Outlined.Cancel)
Text("Cancel Flow")
Text(stringResource(Res.string.adb_commander_cancel_flow))
}
}
FloconIconButton(onClick = onClearConsole) {
FloconIconButton(onClick = { onAction(AdbCommanderAction.ClearConsole) }) {
FloconIcon(imageVector = Icons.Outlined.DeleteSweep)
}
}
@ -132,23 +144,15 @@ private fun FlowExecutionView(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val statusIcon = when (step.status) {
"Completed" -> "+"
"Failed" -> "x"
"Running" -> ">"
"WaitingDelay" -> "~"
"Skipped" -> "-"
else -> " "
}
FloconIcon(
imageVector = step.status.toIcon(),
tint = step.status.toColor(),
modifier = Modifier.size(16.dp),
)
Text(
text = "[$statusIcon] ${step.label}",
text = step.label,
style = FloconTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
color = when (step.status) {
"Completed" -> FloconTheme.colorPalette.onPrimary
"Failed" -> FloconTheme.colorPalette.error
"Running", "WaitingDelay" -> FloconTheme.colorPalette.accent
else -> FloconTheme.colorPalette.onPrimary.copy(alpha = 0.5f)
},
color = step.status.toColor(),
)
if (step.isActive) {
FloconCircularProgressIndicator()
@ -157,3 +161,21 @@ private fun FlowExecutionView(
}
}
}
@Composable
private fun String.toIcon(): ImageVector = when (this) {
"Completed" -> Icons.Outlined.Check
"Failed" -> Icons.Outlined.Close
"Running" -> Icons.Outlined.PlayArrow
"WaitingDelay" -> Icons.Outlined.HourglassEmpty
"Skipped" -> Icons.Outlined.RemoveCircle
else -> Icons.Outlined.Schedule
}
@Composable
private fun String.toColor(): Color = when (this) {
"Completed" -> FloconTheme.colorPalette.onPrimary
"Failed" -> FloconTheme.colorPalette.error
"Running", "WaitingDelay" -> FloconTheme.colorPalette.accent
else -> FloconTheme.colorPalette.onPrimary.copy(alpha = 0.5f)
}