feat: implement history and save detection result to local database

This commit is contained in:
Cutiful 2025-06-29 07:51:50 +07:00
parent 5151379922
commit 287d5774e8
5 changed files with 65 additions and 6 deletions

View File

@ -11,6 +11,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di.cameraModule import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di.cameraModule
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di.databaseModule
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di.diagnosisModule import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di.diagnosisModule
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.navigation.NavGraph import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.navigation.NavGraph
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
@ -21,7 +22,7 @@ class PlantDiseaseDetectionActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// load Module // load Module
loadKoinModules(listOf(cameraModule, diagnosisModule)) loadKoinModules(listOf(cameraModule, diagnosisModule, databaseModule))
Log.d("KOIN_DEBUG", "✅ cameraModule loaded, yeay 🤗") Log.d("KOIN_DEBUG", "✅ cameraModule loaded, yeay 🤗")
@ -39,7 +40,7 @@ class PlantDiseaseDetectionActivity : ComponentActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unloadKoinModules(listOf(cameraModule, diagnosisModule)) unloadKoinModules(listOf(cameraModule, diagnosisModule, databaseModule))
Log.d("KOIN_DEBUG", "🧹 cameraModule unloaded") Log.d("KOIN_DEBUG", "🧹 cameraModule unloaded")
} }

View File

@ -5,6 +5,7 @@ import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.network.
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisRepository import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisRepository
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisRepositoryImpl import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisRepositoryImpl
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.PlantDiagnosisViewModel import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.PlantDiagnosisViewModel
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history.HistoryViewModel
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -43,5 +44,6 @@ val diagnosisModule = module {
get<Retrofit>().create(GeminiApiService::class.java) get<Retrofit>().create(GeminiApiService::class.java)
} }
single<PlantDiagnosisRepository> { PlantDiagnosisRepositoryImpl(get()) } single<PlantDiagnosisRepository> { PlantDiagnosisRepositoryImpl(get()) }
viewModel { PlantDiagnosisViewModel(get()) } viewModel { PlantDiagnosisViewModel(get(), get()) }
viewModel { HistoryViewModel(get()) }
} }

View File

@ -2,27 +2,48 @@ package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.CameraScreen import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.CameraScreen
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.CameraViewModel import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.CameraViewModel
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.DetailScreen import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.DetailScreen
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history.DetailHistoryScreen
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history.HistoryScreen
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history.HistoryViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun NavGraph(navController: NavHostController) { fun NavGraph(navController: NavHostController) {
val cameraViewModel: CameraViewModel = koinViewModel() val cameraViewModel: CameraViewModel = koinViewModel()
val historyViewModel: HistoryViewModel = koinViewModel()
NavHost(navController, startDestination = "camera_screen") { NavHost(navController, startDestination = "camera_screen") {
composable("camera_screen") { composable("camera_screen") {
CameraScreen(navController, cameraViewModel) CameraScreen(navController, cameraViewModel)
} }
composable("detail_screen"){ composable("detail_screen") {
DetailScreen( DetailScreen(
cameraViewModel = cameraViewModel, cameraViewModel = cameraViewModel,
onBack = { navController.popBackStack() } onBack = { navController.popBackStack() }
) )
} }
composable("history") {
HistoryScreen(navController = navController, viewModel = historyViewModel)
}
composable(
route = "detail-history/{id}",
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
val id = backStackEntry.arguments?.getInt("id") ?: return@composable
DetailHistoryScreen(
navController = navController,
diagnosisId = id,
viewModel = historyViewModel
)
}
} }
} }

View File

@ -37,6 +37,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.AppButton
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.AppConstant import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.AppConstant
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.CameraViewModel import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.CameraViewModel
@ -53,6 +54,7 @@ fun DetailScreen(
val bitmap by cameraViewModel.bitmap.collectAsState() val bitmap by cameraViewModel.bitmap.collectAsState()
val diagnosisState by diagnosisViewModel.state.collectAsState() val diagnosisState by diagnosisViewModel.state.collectAsState()
val saveLoading by diagnosisViewModel.saveLoadingState.collectAsState()
LaunchedEffect(bitmap) { LaunchedEffect(bitmap) {
if (bitmap != null && diagnosisState == null) { if (bitmap != null && diagnosisState == null) {
@ -101,6 +103,7 @@ fun DetailScreen(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
) { ) {
Image( Image(
modifier = Modifier.fillMaxSize(),
bitmap = it.asImageBitmap(), bitmap = it.asImageBitmap(),
contentDescription = "Gambar Tanaman", contentDescription = "Gambar Tanaman",
contentScale = ContentScale.Crop, // Make image fill the Card contentScale = ContentScale.Crop, // Make image fill the Card
@ -141,7 +144,6 @@ fun DetailScreen(
) )
} }
} else if (data?.diagnosis != null) { } else if (data?.diagnosis != null) {
// Tampilkan detail diagnosis
Box( Box(
modifier = Modifier modifier = Modifier
.background( .background(
@ -213,6 +215,12 @@ fun DetailScreen(
} }
} }
} }
AppButton(
label = if (saveLoading) "Menyimpan..." else "Simpan Hasil Deteksi",
isEnable = !saveLoading
) {
diagnosisViewModel.saveResultToLocal(data, bitmap!!)
}
} else { } else {
Text("Gagal parsing response") Text("Gagal parsing response")
} }

View File

@ -5,18 +5,26 @@ import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBase64 import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBase64
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toByteArray
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.PlantDiseaseDetectionResponse import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.PlantDiseaseDetectionResponse
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisLocalRepository
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisRepository import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PlantDiagnosisViewModel( class PlantDiagnosisViewModel(
private val repository: PlantDiagnosisRepository private val repository: PlantDiagnosisRepository,
private val localRepository: PlantDiagnosisLocalRepository
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<Result<PlantDiseaseDetectionResponse>?>(null) private val _state = MutableStateFlow<Result<PlantDiseaseDetectionResponse>?>(null)
val state: StateFlow<Result<PlantDiseaseDetectionResponse>?> = _state val state: StateFlow<Result<PlantDiseaseDetectionResponse>?> = _state
private val _saveLoadingState = MutableStateFlow(false)
val saveLoadingState: StateFlow<Boolean> = _saveLoadingState
fun analyzePlant(bitmap: Bitmap, prompt: String) { fun analyzePlant(bitmap: Bitmap, prompt: String) {
Log.d("PlantDiagnosisViewModel", "analyzePlant() called") Log.d("PlantDiagnosisViewModel", "analyzePlant() called")
viewModelScope.launch { viewModelScope.launch {
@ -31,4 +39,23 @@ class PlantDiagnosisViewModel(
} }
} }
} }
fun saveResultToLocal(response: PlantDiseaseDetectionResponse, image: Bitmap) {
_saveLoadingState.value = true
viewModelScope.launch {
delay(1000L)
val entity = PlantDiagnosisEntity(
diagnosis = response.diagnosis ?: "Unknown",
cause = response.cause ?: "-",
description = response.description ?: "-",
prevention = response.prevention?.joinToString(", ") ?: "-",
image = image.toByteArray(),
treatment = response.treatment?.joinToString(", ") {
"${it.method}: ${it.description}"
} ?: "-",
)
localRepository.saveDiagnosis(entity)
_saveLoadingState.value = false
}
}
} }