feat: implement plant disease detection feature
This commit is contained in:
parent
69b7cbac0f
commit
31c70e7a3f
Binary file not shown.
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di.cameraModule
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di.diagnosisModule
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.navigation.NavGraph
|
||||||
|
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
|
||||||
|
import org.koin.core.context.loadKoinModules
|
||||||
|
import org.koin.core.context.unloadKoinModules
|
||||||
|
|
||||||
|
class PlantDiseaseDetectionActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// load Module
|
||||||
|
loadKoinModules(listOf(cameraModule, diagnosisModule))
|
||||||
|
|
||||||
|
Log.d("KOIN_DEBUG", "✅ cameraModule loaded, yeay 🤗")
|
||||||
|
|
||||||
|
enableEdgeToEdge()
|
||||||
|
if (!hasRequiredPermissions()) {
|
||||||
|
ActivityCompat.requestPermissions(this, CAMERAX_PERMISSIONS, 0)
|
||||||
|
}
|
||||||
|
setContent {
|
||||||
|
AgrilinkVocproTheme {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
NavGraph(navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
unloadKoinModules(listOf(cameraModule, diagnosisModule))
|
||||||
|
Log.d("KOIN_DEBUG", "🧹 cameraModule unloaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasRequiredPermissions(): Boolean {
|
||||||
|
return CAMERAX_PERMISSIONS.all {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
applicationContext,
|
||||||
|
it
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val CAMERAX_PERMISSIONS = arrayOf(
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
fun cropToSquare(bitmap: Bitmap?): Bitmap {
|
||||||
|
if (bitmap == null) {
|
||||||
|
return createBitmap(100, 100)
|
||||||
|
} else {
|
||||||
|
val dimension = minOf(bitmap.width, bitmap.height)
|
||||||
|
val xOffset = (bitmap.width - dimension) / 2
|
||||||
|
val yOffset = (bitmap.height - dimension) / 2
|
||||||
|
return Bitmap.createBitmap(bitmap, xOffset, yOffset, dimension, dimension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GeminiRequest(
|
||||||
|
val contents: List<GeminiContent>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GeminiContent(
|
||||||
|
val parts: List<GeminiPart>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GeminiPart(
|
||||||
|
val text: String? = null,
|
||||||
|
val inline_data: InlineData? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InlineData(
|
||||||
|
val mime_type: String,
|
||||||
|
val data: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GeminiResponse(
|
||||||
|
val candidates: List<Candidate>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Candidate(
|
||||||
|
val avgLogprobs: Double,
|
||||||
|
val content: Content,
|
||||||
|
val finishReason: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Content(
|
||||||
|
val parts: List<Part>,
|
||||||
|
val role: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Part(
|
||||||
|
val text: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlantDiseaseDetectionResponse(
|
||||||
|
val cause: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val diagnosis: String? = null,
|
||||||
|
val prevention: List<String>? = null,
|
||||||
|
val treatment: List<Treatment>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Treatment(
|
||||||
|
val description: String? = null,
|
||||||
|
val method: String? = null
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.network
|
||||||
|
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiRequest
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiResponse
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface GeminiApiService {
|
||||||
|
@POST("v1beta/models/gemini-2.0-flash:generateContent")
|
||||||
|
suspend fun detectDisease(
|
||||||
|
@Query("key") apiKey: String,
|
||||||
|
@Body request: GeminiRequest
|
||||||
|
): GeminiResponse
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository
|
||||||
|
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.PlantDiseaseDetectionResponse
|
||||||
|
|
||||||
|
interface PlantDiagnosisRepository {
|
||||||
|
suspend fun detectDisease(base64Image: String, prompt: String): PlantDiseaseDetectionResponse
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiContent
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiPart
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiRequest
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.InlineData
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.PlantDiseaseDetectionResponse
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.network.GeminiApiService
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class PlantDiagnosisRepositoryImpl(private val api: GeminiApiService) : PlantDiagnosisRepository {
|
||||||
|
override suspend fun detectDisease(
|
||||||
|
base64Image: String,
|
||||||
|
prompt: String
|
||||||
|
): PlantDiseaseDetectionResponse {
|
||||||
|
val request = GeminiRequest(
|
||||||
|
contents = listOf(
|
||||||
|
GeminiContent(
|
||||||
|
parts = listOf(
|
||||||
|
GeminiPart(text = prompt),
|
||||||
|
GeminiPart(inline_data = InlineData("image/jpeg", base64Image))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = api.detectDisease(
|
||||||
|
apiKey = "AIzaSyDYZHlMfOdFAcTFptWHqhPwIO734VdNUWE", // Ideally use BuildConfig or secure storage
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
Log.d("PlantDiagnosisRepository", "Response: $response")
|
||||||
|
|
||||||
|
val rawJson = response.candidates.first().content.parts.first().text
|
||||||
|
val cleanedJson = rawJson
|
||||||
|
.replace("```json", "")
|
||||||
|
.replace("```", "")
|
||||||
|
.trim()
|
||||||
|
return Json.decodeFromString(cleanedJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,5 +8,5 @@ import org.koin.dsl.module
|
||||||
|
|
||||||
val cameraModule = module {
|
val cameraModule = module {
|
||||||
single<CameraRepository> { CameraRepositoryImpl() }
|
single<CameraRepository> { CameraRepositoryImpl() }
|
||||||
viewModel { CameraViewModel(get(), get())}
|
viewModel { CameraViewModel(get())}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di
|
||||||
|
|
||||||
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.network.GeminiApiService
|
||||||
|
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.presentation.detail.PlantDiagnosisViewModel
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
val diagnosisModule = module {
|
||||||
|
single {
|
||||||
|
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.addInterceptor(loggingInterceptor)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
val contentType = "application/json".toMediaType()
|
||||||
|
val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Retrofit.Builder()
|
||||||
|
.baseUrl("https://generativelanguage.googleapis.com/")
|
||||||
|
.client(get()) // ambil OkHttpClient dari Koin
|
||||||
|
.addConverterFactory(json.asConverterFactory(contentType))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
single<GeminiApiService> {
|
||||||
|
get<Retrofit>().create(GeminiApiService::class.java)
|
||||||
|
}
|
||||||
|
single<PlantDiagnosisRepository> { PlantDiagnosisRepositoryImpl(get()) }
|
||||||
|
viewModel { PlantDiagnosisViewModel(get()) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
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.detail.DetailScreen
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavGraph(navController: NavHostController) {
|
||||||
|
val cameraViewModel: CameraViewModel = koinViewModel()
|
||||||
|
|
||||||
|
|
||||||
|
NavHost(navController, startDestination = "camera_screen") {
|
||||||
|
composable("camera_screen") {
|
||||||
|
CameraScreen(navController, cameraViewModel)
|
||||||
|
}
|
||||||
|
composable("detail_screen"){
|
||||||
|
DetailScreen(
|
||||||
|
cameraViewModel = cameraViewModel,
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,6 @@ 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.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -48,12 +47,7 @@ fun CameraScreen(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
// val bitmaps by viewModel.bitmaps.collectAsState()
|
|
||||||
val bitmap by viewModel.bitmap.collectAsState()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
// val scaffoldState = rememberBottomSheetScaffoldState()
|
|
||||||
|
|
||||||
val controller = remember {
|
val controller = remember {
|
||||||
LifecycleCameraController(context).apply {
|
LifecycleCameraController(context).apply {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ private const val TAG = "CameraViewModel"
|
||||||
|
|
||||||
class CameraViewModel(
|
class CameraViewModel(
|
||||||
private val cameraRepository: CameraRepository,
|
private val cameraRepository: CameraRepository,
|
||||||
private val diagnosisViewModel: PlantDiagnosisViewModel
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -26,7 +25,6 @@ class CameraViewModel(
|
||||||
private val _bitmap = MutableStateFlow<Bitmap?>(null)
|
private val _bitmap = MutableStateFlow<Bitmap?>(null)
|
||||||
val bitmap: StateFlow<Bitmap?> = _bitmap
|
val bitmap: StateFlow<Bitmap?> = _bitmap
|
||||||
|
|
||||||
val prompt = AppConstant().prompt
|
|
||||||
|
|
||||||
fun takePicture(
|
fun takePicture(
|
||||||
controller: LifecycleCameraController,
|
controller: LifecycleCameraController,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.R
|
||||||
|
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomCameraShutter(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onShutterClick: () -> Unit = {},
|
||||||
|
onFlashClick: () -> Unit = {},
|
||||||
|
onGalleryClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 44.dp, vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// Tombol Flash
|
||||||
|
IconButton(
|
||||||
|
onClick = onFlashClick,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
painter = painterResource(id = R.drawable.flash),
|
||||||
|
contentDescription = "Flash",
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onShutterClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.background(MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f), shape = CircleShape)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.background(MainGreen, shape = CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onGalleryClick,
|
||||||
|
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
painter = painterResource(id = R.drawable.solar_gallery_broken),
|
||||||
|
contentDescription = "Gallery",
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CameraShutterPreview() {
|
||||||
|
CustomCameraShutter()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
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
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.syaroful.agrilinkvocpro.R
|
||||||
|
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.presentation.camera.CameraViewModel
|
||||||
|
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DetailScreen(
|
||||||
|
cameraViewModel: CameraViewModel,
|
||||||
|
onBack: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val diagnosisViewModel: PlantDiagnosisViewModel = koinViewModel()
|
||||||
|
|
||||||
|
val bitmap by cameraViewModel.bitmap.collectAsState()
|
||||||
|
val diagnosisState by diagnosisViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(bitmap) {
|
||||||
|
if (bitmap != null && diagnosisState == null) {
|
||||||
|
diagnosisViewModel.analyzePlant(bitmap = bitmap!!, prompt = AppConstant().prompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
"Deteksi Penyakit Tanaman",
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(24.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
10.dp,
|
||||||
|
Alignment.Top
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
bitmap?.let {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 180.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = it.asImageBitmap(),
|
||||||
|
contentDescription = "Gambar Tanaman",
|
||||||
|
contentScale = ContentScale.Crop, // Make image fill the Card
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (val result = diagnosisState) {
|
||||||
|
null -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = MainGreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
if (result.isFailure) {
|
||||||
|
DefaultErrorComponent(
|
||||||
|
painter = painterResource(id = R.drawable.mascot_confused),
|
||||||
|
label = "Oops!",
|
||||||
|
message = result.exceptionOrNull()?.message.toString()
|
||||||
|
)
|
||||||
|
} else if (result.isSuccess) {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
if (data?.diagnosis == "Sehat") {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = Color.Green,
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Tanaman Sehat",
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (data?.diagnosis != null) {
|
||||||
|
// Tampilkan detail diagnosis
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = Color.Red,
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Terkena Penyakit",
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "Penyakit Terdeteksi :",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color(0xFFFF3D3D)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
data.diagnosis ?: "Unknown",
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Penyebab :",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color(0xFFFF3D3D)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
data.cause ?: "Unknown",
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
TextCardComponent(
|
||||||
|
label = "Informasi Umum",
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = data.description ?: "Unknown",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextCardComponent(
|
||||||
|
label = "Pengobatan",
|
||||||
|
) {
|
||||||
|
data.treatment?.forEach {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
it.method ?: "Unknown",
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
it.description ?: "Unknown",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextCardComponent(
|
||||||
|
label = "Pencegahan",
|
||||||
|
) {
|
||||||
|
data.prevention?.forEach {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Gagal parsing response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextCardComponent(
|
||||||
|
label: String,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
10.dp,
|
||||||
|
Alignment.Top
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBase64
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.PlantDiseaseDetectionResponse
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisRepository
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PlantDiagnosisViewModel(
|
||||||
|
private val repository: PlantDiagnosisRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow<Result<PlantDiseaseDetectionResponse>?>(null)
|
||||||
|
val state: StateFlow<Result<PlantDiseaseDetectionResponse>?> = _state
|
||||||
|
|
||||||
|
fun analyzePlant(bitmap: Bitmap, prompt: String) {
|
||||||
|
Log.d("PlantDiagnosisViewModel", "analyzePlant() called")
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val base64 = bitmap.toBase64()
|
||||||
|
val result = repository.detectDisease(prompt = prompt, base64Image = base64)
|
||||||
|
_state.value = Result.success(result)
|
||||||
|
Log.d("PlantDiagnosisViewModel", "Diagnosis result: $result")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.value = Result.failure(e)
|
||||||
|
Log.e("PlantDiagnosisViewModel", "Error during diagnosis", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user