feat: enhance UI/UX of plant disease detection feature
This commit is contained in:
parent
0dae042e54
commit
90df005a3f
|
|
@ -20,7 +20,7 @@ fun NavGraph(navController: NavHostController) {
|
||||||
val historyViewModel: HistoryViewModel = koinViewModel()
|
val historyViewModel: HistoryViewModel = koinViewModel()
|
||||||
|
|
||||||
|
|
||||||
NavHost(navController, startDestination = "camera_screen") {
|
NavHost(navController, startDestination = "history") {
|
||||||
composable("camera_screen") {
|
composable("camera_screen") {
|
||||||
CameraScreen(navController, cameraViewModel)
|
CameraScreen(navController, cameraViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
|
@ -68,6 +70,7 @@ fun DetailScreen(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
"Deteksi Penyakit Tanaman",
|
"Deteksi Penyakit Tanaman",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
@ -82,14 +85,15 @@ fun DetailScreen(
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.padding(24.dp),
|
.padding(horizontal = 16.dp),
|
||||||
contentAlignment = Alignment.TopCenter
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
10.dp,
|
10.dp,
|
||||||
Alignment.Top
|
Alignment.Top
|
||||||
|
|
@ -215,12 +219,7 @@ fun DetailScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppButton(
|
Spacer(modifier = Modifier.height(56.dp))
|
||||||
label = if (saveLoading) "Menyimpan..." else "Simpan Hasil Deteksi",
|
|
||||||
isEnable = !saveLoading
|
|
||||||
) {
|
|
||||||
diagnosisViewModel.saveResultToLocal(data, bitmap!!)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Text("Gagal parsing response")
|
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!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ fun DetailHistoryScreen(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = plantDiagnosis?.treatment ?: "-",
|
text = plantDiagnosis?.treatment?.replace("., ", ".\n\n") ?: "-",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +173,7 @@ fun DetailHistoryScreen(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = plantDiagnosis?.prevention ?: "-",
|
text = plantDiagnosis?.prevention?.replace("., ", ".\n\n") ?: "-",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -28,17 +27,23 @@ import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.syaroful.agrilinkvocpro.R
|
import com.syaroful.agrilinkvocpro.R
|
||||||
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
|
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.toBitmap
|
||||||
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toFormattedDate
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -47,50 +52,85 @@ fun HistoryScreen(
|
||||||
viewModel: HistoryViewModel
|
viewModel: HistoryViewModel
|
||||||
) {
|
) {
|
||||||
val diagnoses by viewModel.diagnoses.collectAsState()
|
val diagnoses by viewModel.diagnoses.collectAsState()
|
||||||
|
val fillAndPadding = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
"Riwayat Deteksi",
|
"Deteksi Penyakit Tanaman",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
navController.popBackStack()
|
|
||||||
}) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
|
val baseModifier = fillAndPadding.then(Modifier.padding(innerPadding))
|
||||||
|
|
||||||
when {
|
if (diagnoses.isEmpty()) {
|
||||||
|
Column(
|
||||||
(diagnoses.isEmpty()) -> DefaultErrorComponent(
|
modifier = baseModifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
DiseaseDetectionBanner()
|
||||||
|
StartDiagnosisButton { navController.navigate("camera_screen") }
|
||||||
|
DefaultErrorComponent(
|
||||||
label = "Waduh!",
|
label = "Waduh!",
|
||||||
message = "Ga ada data sama sekali, coba tambahkan",
|
message = "Ga ada data sama sekali, coba tambahkan",
|
||||||
painter = painterResource(id = R.drawable.mascot_surprised)
|
painter = painterResource(id = R.drawable.mascot_surprised)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
else -> LazyColumn(
|
} else {
|
||||||
modifier = Modifier
|
LazyColumn(
|
||||||
.padding(innerPadding)
|
modifier = baseModifier,
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
item { DiseaseDetectionBanner() }
|
||||||
|
item { StartDiagnosisButton { navController.navigate("camera_screen") } }
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Riwayat Deteksi",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
items(diagnoses) { diagnosis ->
|
items(diagnoses) { diagnosis ->
|
||||||
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
navController.currentBackStackEntry?.savedStateHandle?.set(
|
navController.currentBackStackEntry?.savedStateHandle?.set("plantData", diagnosis)
|
||||||
"plantData",
|
|
||||||
diagnosis
|
|
||||||
)
|
|
||||||
navController.navigate("detail-history/${diagnosis.id}")
|
navController.navigate("detail-history/${diagnosis.id}")
|
||||||
}
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -99,11 +139,7 @@ fun HistoryScreen(
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(modifier = Modifier.padding(12.dp).fillMaxWidth()) {
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(80.dp)
|
.size(80.dp)
|
||||||
|
|
@ -115,22 +151,66 @@ fun HistoryScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier.height(80.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Diagnosis: ${diagnosis.diagnosis}",
|
"Diagnosis: ${diagnosis.diagnosis}",
|
||||||
style = MaterialTheme.typography.labelLarge
|
style = MaterialTheme.typography.labelLarge
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
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(
|
Text(
|
||||||
diagnosis.timestamp.toFormattedDate(),
|
diagnosis.timestamp.toFormattedDate(),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
textAlign = TextAlign.End
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user