From 90df005a3ff88a979df86132d18969b33dfc3c84 Mon Sep 17 00:00:00 2001 From: Cutiful <113351087+Syaroful@users.noreply.github.com> Date: Sat, 5 Jul 2025 11:25:42 +0700 Subject: [PATCH] feat: enhance UI/UX of plant disease detection feature --- .../navigation/NavGraph.kt | 2 +- .../presentation/detail/DetailScreen.kt | 29 ++- .../history/DetailHistoryScreen.kt | 4 +- .../presentation/history/HistoryScreen.kt | 220 ++++++++++++------ 4 files changed, 173 insertions(+), 82 deletions(-) diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/navigation/NavGraph.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/navigation/NavGraph.kt index 27c8bb6..351bc8b 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/navigation/NavGraph.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/navigation/NavGraph.kt @@ -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) } diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/DetailScreen.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/DetailScreen.kt index c43ad01..ccca72a 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/DetailScreen.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/DetailScreen.kt @@ -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!!) + } + } + } } } } diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt index 4cd5082..21aefa9 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt @@ -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 ) } diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/HistoryScreen.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/HistoryScreen.kt index 48ac767..f29f796 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/HistoryScreen.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/HistoryScreen.kt @@ -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) } } } } -} \ No newline at end of file +} + +@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", + ) + } + } +}