feat: enhance UI/UX of plant disease detection feature

This commit is contained in:
Cutiful 2025-07-05 11:25:42 +07:00
parent 0dae042e54
commit 90df005a3f
4 changed files with 173 additions and 82 deletions

View File

@ -20,7 +20,7 @@ fun NavGraph(navController: NavHostController) {
val historyViewModel: HistoryViewModel = koinViewModel()
NavHost(navController, startDestination = "camera_screen") {
NavHost(navController, startDestination = "history") {
composable("camera_screen") {
CameraScreen(navController, cameraViewModel)
}

View File

@ -5,8 +5,10 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@ -68,6 +70,7 @@ fun DetailScreen(
title = {
Text(
"Deteksi Penyakit Tanaman",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
@ -82,14 +85,15 @@ fun DetailScreen(
) { innerPadding ->
Box(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxSize()
.padding(innerPadding)
.padding(24.dp),
.padding(horizontal = 16.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(
10.dp,
Alignment.Top
@ -215,12 +219,7 @@ fun DetailScreen(
}
}
}
AppButton(
label = if (saveLoading) "Menyimpan..." else "Simpan Hasil Deteksi",
isEnable = !saveLoading
) {
diagnosisViewModel.saveResultToLocal(data, bitmap!!)
}
Spacer(modifier = Modifier.height(56.dp))
} else {
Text("Gagal parsing response")
}
@ -228,6 +227,18 @@ fun DetailScreen(
}
}
}
diagnosisState?.let { result ->
if (!result.isFailure && result.isSuccess && result.getOrNull()?.diagnosis != null) {
AppButton(
modifier = Modifier
.align(Alignment.BottomCenter),
label = if (saveLoading) "Menyimpan..." else "Simpan Hasil Deteksi",
isEnable = !saveLoading
) {
diagnosisViewModel.saveResultToLocal(result.getOrNull()!!, bitmap!!)
}
}
}
}
}
}

View File

@ -164,7 +164,7 @@ fun DetailHistoryScreen(
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = plantDiagnosis?.treatment ?: "-",
text = plantDiagnosis?.treatment?.replace("., ", ".\n\n") ?: "-",
style = MaterialTheme.typography.bodyMedium
)
}
@ -173,7 +173,7 @@ fun DetailHistoryScreen(
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = plantDiagnosis?.prevention ?: "-",
text = plantDiagnosis?.prevention?.replace("., ", ".\n\n") ?: "-",
style = MaterialTheme.typography.bodyMedium
)
}

View File

@ -1,5 +1,6 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -16,11 +17,9 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@ -28,17 +27,23 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBitmap
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toFormattedDate
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -47,90 +52,165 @@ fun HistoryScreen(
viewModel: HistoryViewModel
) {
val diagnoses by viewModel.diagnoses.collectAsState()
val fillAndPadding = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"Riwayat Deteksi",
"Deteksi Penyakit Tanaman",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
},
navigationIcon = {
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
)
}
) { innerPadding ->
val baseModifier = fillAndPadding.then(Modifier.padding(innerPadding))
when {
(diagnoses.isEmpty()) -> DefaultErrorComponent(
label = "Waduh!",
message = "Ga ada data sama sekali, coba tambahkan",
painter = painterResource(id = R.drawable.mascot_surprised)
)
else -> LazyColumn(
modifier = Modifier
.padding(innerPadding)
.padding(16.dp),
if (diagnoses.isEmpty()) {
Column(
modifier = baseModifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
DiseaseDetectionBanner()
StartDiagnosisButton { navController.navigate("camera_screen") }
DefaultErrorComponent(
label = "Waduh!",
message = "Ga ada data sama sekali, coba tambahkan",
painter = painterResource(id = R.drawable.mascot_surprised)
)
}
} else {
LazyColumn(
modifier = baseModifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item { DiseaseDetectionBanner() }
item { StartDiagnosisButton { navController.navigate("camera_screen") } }
item {
Text(
"Riwayat Deteksi",
style = MaterialTheme.typography.titleMedium
)
}
items(diagnoses) { diagnosis ->
Box(
modifier = Modifier
.clickable {
navController.currentBackStackEntry?.savedStateHandle?.set(
"plantData",
diagnosis
)
navController.navigate("detail-history/${diagnosis.id}")
}
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Box(
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(4.dp))
) {
Image(
bitmap = diagnosis.image.toBitmap().asImageBitmap(),
contentDescription = "Diagnosis Image"
)
}
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
"Diagnosis: ${diagnosis.diagnosis}",
style = MaterialTheme.typography.labelLarge
)
Spacer(modifier = Modifier.height(12.dp))
Text(
diagnosis.timestamp.toFormattedDate(),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.End
)
}
}
}
DiagnosisListItem(diagnosis, navController)
}
}
}
}
}
}
@Composable
private fun StartDiagnosisButton(onClick: () -> Unit) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MainGreen
),
shape = RoundedCornerShape(8.dp),
border = BorderStroke(color = MainGreen, width = 1.dp)
) {
Text("Mulai diagnosis")
}
}
@Composable
private fun DiagnosisListItem(
diagnosis: PlantDiagnosisEntity,
navController: NavHostController
) {
Box(
modifier = Modifier
.clickable {
navController.currentBackStackEntry?.savedStateHandle?.set("plantData", diagnosis)
navController.navigate("detail-history/${diagnosis.id}")
}
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
) {
Row(modifier = Modifier.padding(12.dp).fillMaxWidth()) {
Box(
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(4.dp))
) {
Image(
bitmap = diagnosis.image.toBitmap().asImageBitmap(),
contentDescription = "Diagnosis Image"
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier.height(80.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
"Diagnosis: ${diagnosis.diagnosis}",
style = MaterialTheme.typography.labelLarge
)
Text(
diagnosis.description,
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row {
Image(
modifier = Modifier.size(14.dp),
painter = painterResource(R.drawable.ic_calendar),
contentDescription = "Date Icon",
colorFilter = ColorFilter.tint(Color.Gray)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
diagnosis.timestamp.toFormattedDate(),
style = MaterialTheme.typography.bodySmall,
)
}
}
}
}
}
@Composable
private fun DiseaseDetectionBanner() {
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = MainGreen, shape = RoundedCornerShape(8.dp))
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.fillMaxWidth(0.6f)) {
Text(
"Lindungi Tanamanmu dari Hama dan Penyakit",
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
)
Text(
"Deteksi, lihat hasil dan lakukan perawatan",
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
)
}
Image(
modifier = Modifier.fillMaxWidth(0.8f),
painter = painterResource(id = R.drawable.plant_in_pot),
contentDescription = "Plant Image",
)
}
}
}