feat: add custom snackbar and UI events for control feature

This commit is contained in:
Cutiful 2025-07-05 11:24:41 +07:00
parent 0f65748195
commit 1ef33bcfda
4 changed files with 125 additions and 1 deletions

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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()
}

View File

@ -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.control_feature.data.repository.ControlRepository
import com.syaroful.agrilinkvocpro.data.UserPreferences import com.syaroful.agrilinkvocpro.data.UserPreferences
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -36,6 +38,13 @@ class ControlViewModel(
private val _isLoading = MutableStateFlow<Boolean>(false) private val _isLoading = MutableStateFlow<Boolean>(false)
val isLoading: StateFlow<Boolean> = _isLoading val isLoading: StateFlow<Boolean> = _isLoading
private val _uiEvent = MutableSharedFlow<ControlUiEvent>()
val uiEvent = _uiEvent.asSharedFlow()
init {
getActuatorStatus()
}
fun getActuatorStatus() { fun getActuatorStatus() {
_allActuatorState.value = ControlState.Loading _allActuatorState.value = ControlState.Loading
viewModelScope.launch { viewModelScope.launch {
@ -55,6 +64,7 @@ class ControlViewModel(
ActuatorType.PUMP.displayName -> _pumpStatus.value = isOn ActuatorType.PUMP.displayName -> _pumpStatus.value = isOn
} }
} }
Log.d(TAG, "Successfully get Actuator Status ${response.body()}")
} else { } else {
val errorBody = response.errorBody()?.string() val errorBody = response.errorBody()?.string()
val errorMessage = errorBody ?: "Error ${response.code()}" val errorMessage = errorBody ?: "Error ${response.code()}"
@ -78,7 +88,11 @@ class ControlViewModel(
val response = when (actuator) { val response = when (actuator) {
ActuatorType.WATER -> repository.controlWaterValve(authHeader, stringState) ActuatorType.WATER -> repository.controlWaterValve(authHeader, stringState)
ActuatorType.NUTRIENT -> repository.controlNutrientValve(authHeader, stringState) ActuatorType.NUTRIENT -> repository.controlNutrientValve(
authHeader,
stringState
)
ActuatorType.PUMP -> repository.controlPump(authHeader, stringState) ActuatorType.PUMP -> repository.controlPump(authHeader, stringState)
} }
@ -86,12 +100,16 @@ class ControlViewModel(
val isOn = response.body()?.log?.action == "ON" val isOn = response.body()?.log?.action == "ON"
updateActuatorStatus(actuator, isOn) updateActuatorStatus(actuator, isOn)
Log.d(TAG, "Successfully controlled ${actuator.displayName}") 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 { } else {
val errorBody = response.errorBody()?.string() val errorBody = response.errorBody()?.string()
Log.e(TAG, "Error controlling ${actuator.displayName}: $errorBody") Log.e(TAG, "Error controlling ${actuator.displayName}: $errorBody")
_uiEvent.emit(ControlUiEvent.Error("Failed to control ${actuator.displayName}: $errorBody"))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Exception while controlling ${actuator.displayName}: ${e.message}") Log.e(TAG, "Exception while controlling ${actuator.displayName}: ${e.message}")
_uiEvent.emit(ControlUiEvent.Error("Exception controlling ${actuator.displayName}: ${e.localizedMessage}"))
} finally { } finally {
_isLoading.value = false _isLoading.value = false
} }