From 1ef33bcfda5fc79f4d127c3b1ff63ad6b41f09d4 Mon Sep 17 00:00:00 2001 From: Cutiful <113351087+Syaroful@users.noreply.github.com> Date: Sat, 5 Jul 2025 11:24:41 +0700 Subject: [PATCH] feat: add custom snackbar and UI events for control feature --- .../components/CustomSnackbarComponent.kt | 50 +++++++++++++++++++ .../core/components/CustomSnackbar.kt | 50 +++++++++++++++++++ .../presentation/control/ControlUiEvent.kt | 6 +++ .../presentation/control/ControlViewModel.kt | 20 +++++++- 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/CustomSnackbarComponent.kt create mode 100644 agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/CustomSnackbar.kt create mode 100644 agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlUiEvent.kt diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/CustomSnackbarComponent.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/CustomSnackbarComponent.kt new file mode 100644 index 0000000..6f8f8d4 --- /dev/null +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/CustomSnackbarComponent.kt @@ -0,0 +1,50 @@ +package com.syaroful.agrilinkvocpro.core.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun CustomSnackBarComponent( + modifier: Modifier = Modifier, + message: String, + icon: ImageVector +) { + Snackbar( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + shape = RoundedCornerShape(8.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text(message) + Icon( + imageVector = icon, + contentDescription = "Check Icon", + modifier = Modifier.size(24.dp) + ) + } + } +} + +@Preview +@Composable +fun CustomSnackBarPreview() { + CustomSnackBarComponent( + message = "Actuator State Changed", + icon = Icons.Filled.Check + ) +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/CustomSnackbar.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/CustomSnackbar.kt new file mode 100644 index 0000000..8a4c948 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/CustomSnackbar.kt @@ -0,0 +1,50 @@ +package com.syaroful.agrilinkvocpro.control_feature.core.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun CustomSnackBar( + modifier: Modifier = Modifier, + message: String, + icon: ImageVector +) { + Snackbar( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + shape = RoundedCornerShape(8.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text(message) + Icon( + imageVector = icon, + contentDescription = "Check Icon", + modifier = Modifier.size(24.dp) + ) + } + } +} + +@Preview +@Composable +fun CustomSnackBarPreview() { + CustomSnackBar( + message = "Actuator State Changed", + icon = Icons.Filled.Check + ) +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlUiEvent.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlUiEvent.kt new file mode 100644 index 0000000..a95acd3 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlUiEvent.kt @@ -0,0 +1,6 @@ +package com.syaroful.agrilinkvocpro.control_feature.presentation.control + +sealed class ControlUiEvent { + data class Success(val message: String) : ControlUiEvent() + data class Error(val message: String) : ControlUiEvent() +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlViewModel.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlViewModel.kt index 8e8244c..6d60e42 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlViewModel.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlViewModel.kt @@ -9,8 +9,10 @@ import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository import com.syaroful.agrilinkvocpro.data.UserPreferences import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -36,6 +38,13 @@ class ControlViewModel( private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading + private val _uiEvent = MutableSharedFlow() + val uiEvent = _uiEvent.asSharedFlow() + + init { + getActuatorStatus() + } + fun getActuatorStatus() { _allActuatorState.value = ControlState.Loading viewModelScope.launch { @@ -55,6 +64,7 @@ class ControlViewModel( ActuatorType.PUMP.displayName -> _pumpStatus.value = isOn } } + Log.d(TAG, "Successfully get Actuator Status ${response.body()}") } else { val errorBody = response.errorBody()?.string() val errorMessage = errorBody ?: "Error ${response.code()}" @@ -78,7 +88,11 @@ class ControlViewModel( val response = when (actuator) { ActuatorType.WATER -> repository.controlWaterValve(authHeader, stringState) - ActuatorType.NUTRIENT -> repository.controlNutrientValve(authHeader, stringState) + ActuatorType.NUTRIENT -> repository.controlNutrientValve( + authHeader, + stringState + ) + ActuatorType.PUMP -> repository.controlPump(authHeader, stringState) } @@ -86,12 +100,16 @@ class ControlViewModel( val isOn = response.body()?.log?.action == "ON" updateActuatorStatus(actuator, isOn) Log.d(TAG, "Successfully controlled ${actuator.displayName}") + Log.d(TAG, "Successfully controlled ${response.body()}") + _uiEvent.emit(ControlUiEvent.Success("${actuator.displayName} turned ${if (isOn) "ON" else "OFF"}")) } else { val errorBody = response.errorBody()?.string() Log.e(TAG, "Error controlling ${actuator.displayName}: $errorBody") + _uiEvent.emit(ControlUiEvent.Error("Failed to control ${actuator.displayName}: $errorBody")) } } catch (e: Exception) { Log.e(TAG, "Exception while controlling ${actuator.displayName}: ${e.message}") + _uiEvent.emit(ControlUiEvent.Error("Exception controlling ${actuator.displayName}: ${e.localizedMessage}")) } finally { _isLoading.value = false }