From 31c70e7a3fd82e5b0c04ddcf212fb7e6725fa3d3 Mon Sep 17 00:00:00 2001 From: Cutiful <113351087+Syaroful@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:20:47 +0700 Subject: [PATCH] feat: implement plant disease detection feature --- .../drawable/plant_disease_detection_icon.png | Bin 975 -> 4018 bytes .../PlantDiseaseDetectionActivity.kt | 61 +++++ .../core/extention/CropToSquare.kt | 15 ++ .../data/model/GeminiRequest.kt | 25 ++ .../data/model/GeminiResponse.kt | 26 ++ .../model/PlantDiseaseDetectionResponse.kt | 18 ++ .../data/network/GeminiApiService.kt | 15 ++ .../repository/PlantDiagnosisRepository.kt | 7 + .../PlantDiagnosisRepositoryImpl.kt | 41 +++ .../di/CameraModule.kt | 2 +- .../di/PlantDiagnosisModule.kt | 47 ++++ .../navigation/NavGraph.kt | 28 ++ .../presentation/camera/CameraScreen.kt | 6 - .../presentation/camera/CameraViewModel.kt | 2 - .../camera/component/CurtomCameraShutter.kt | 87 ++++++ .../presentation/detail/DetailScreen.kt | 253 ++++++++++++++++++ .../detail/PlantDiagnosisViewModel.kt | 34 +++ 17 files changed, 658 insertions(+), 9 deletions(-) create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/PlantDiseaseDetectionActivity.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/core/extention/CropToSquare.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiRequest.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiResponse.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/PlantDiseaseDetectionResponse.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/network/GeminiApiService.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepository.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepositoryImpl.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/PlantDiagnosisModule.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/navigation/NavGraph.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/component/CurtomCameraShutter.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/DetailScreen.kt create mode 100644 agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/PlantDiagnosisViewModel.kt diff --git a/agrilinkvocpro/app/src/main/res/drawable/plant_disease_detection_icon.png b/agrilinkvocpro/app/src/main/res/drawable/plant_disease_detection_icon.png index 5399523b99f51f69c5ab21b3ce987cd8cea77e56..0df85a47972463f9b517b6b47399652e13fe6fb0 100644 GIT binary patch literal 4018 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 YKW-I_L0PXL$p8QV07*qoM6N<$f;rT5fdBvi delta 960 zcmV;x13&z-AI}FiiBL{Q4GJ0x0000DNk~Le0000P0000O2nGNE01vfR#sB~S32;bR za{vGf6951U69E94oEVWdAAbX6Nkl7oQTDM-s0DcTvX0tI1eVd)r5jFt18dr#-i z=;S18y z;jSJeu=zBoIj}hsgr%T)Aw`JHIgafYFa=UD)ZHKm21$ASLF4U zNz!!$aEp(EF)bS-_j^C*0wPimNR1?DY7?E12RDjY(Q&UJzgf~F^F5@Om{A`P-DLN5 zpHQKKXPpDNfTWb19DnK}R8UDCu_j3S_WDp06#?Q10j)UNQ@figk@legD;I#ohltCp zD!@GRSXD58pfx`y5Rg;|F18Ax8g>B^2m!8>2U34e{48l{0CEb~^h`Ca<6f~+-}I|--wvB@1=5!5jIc2;qhTXv3DvPLc- zV{z>{n-wI@3579U8e`i&)ZQ+)_aS*bWgH$CZJVWlcY!1wfnEw#BvC?AtSfMYv zr;?qD{MA>dndxN?@|f#kTG!m%774WYxg5;@#iSL%VV_wA>g|_%y(?*oAxAqk+;z?< z;#yoWbAQ6DO`Cq@Jt)kdRPZorc*R5li1DeF=Kj@vgKS&Lq;36Jjrj>tcPmyZa`6BF002ovPDHLkU;%>4o~By> diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/PlantDiseaseDetectionActivity.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/PlantDiseaseDetectionActivity.kt new file mode 100644 index 0000000..b822a72 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/PlantDiseaseDetectionActivity.kt @@ -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 + ) + } + +} \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/core/extention/CropToSquare.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/core/extention/CropToSquare.kt new file mode 100644 index 0000000..25276ea --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/core/extention/CropToSquare.kt @@ -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) + } +} \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiRequest.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiRequest.kt new file mode 100644 index 0000000..3c24d75 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiRequest.kt @@ -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 +) + +@Serializable +data class GeminiContent( + val parts: List +) + +@Serializable +data class GeminiPart( + val text: String? = null, + val inline_data: InlineData? = null +) + +@Serializable +data class InlineData( + val mime_type: String, + val data: String +) diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiResponse.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiResponse.kt new file mode 100644 index 0000000..12e24f5 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/GeminiResponse.kt @@ -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, +) + +@Serializable +data class Candidate( + val avgLogprobs: Double, + val content: Content, + val finishReason: String +) + +@Serializable +data class Content( + val parts: List, + val role: String +) + +@Serializable +data class Part( + val text: String +) \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/PlantDiseaseDetectionResponse.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/PlantDiseaseDetectionResponse.kt new file mode 100644 index 0000000..2abfa77 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/model/PlantDiseaseDetectionResponse.kt @@ -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? = null, + val treatment: List? = null +) + +@Serializable +data class Treatment( + val description: String? = null, + val method: String? = null +) \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/network/GeminiApiService.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/network/GeminiApiService.kt new file mode 100644 index 0000000..cf53632 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/network/GeminiApiService.kt @@ -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 +} \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepository.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepository.kt new file mode 100644 index 0000000..c4fca01 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepository.kt @@ -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 +} \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepositoryImpl.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepositoryImpl.kt new file mode 100644 index 0000000..64faea5 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/data/repository/PlantDiagnosisRepositoryImpl.kt @@ -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) + } +} \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/CameraModule.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/CameraModule.kt index 94746e6..8effc1b 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/CameraModule.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/CameraModule.kt @@ -8,5 +8,5 @@ import org.koin.dsl.module val cameraModule = module { single { CameraRepositoryImpl() } - viewModel { CameraViewModel(get(), get())} + viewModel { CameraViewModel(get())} } \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/PlantDiagnosisModule.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/PlantDiagnosisModule.kt new file mode 100644 index 0000000..2fbbe74 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/di/PlantDiagnosisModule.kt @@ -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 { + get().create(GeminiApiService::class.java) + } + single { PlantDiagnosisRepositoryImpl(get()) } + viewModel { PlantDiagnosisViewModel(get()) } +} \ No newline at end of file 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 new file mode 100644 index 0000000..7cd5bc5 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/navigation/NavGraph.kt @@ -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() } + ) + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraScreen.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraScreen.kt index 17b5c7d..648bf28 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraScreen.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -48,12 +47,7 @@ fun CameraScreen( val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - 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 { LifecycleCameraController(context).apply { diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraViewModel.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraViewModel.kt index fccc0f9..75815d8 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraViewModel.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/CameraViewModel.kt @@ -16,7 +16,6 @@ private const val TAG = "CameraViewModel" class CameraViewModel( private val cameraRepository: CameraRepository, - private val diagnosisViewModel: PlantDiagnosisViewModel ) : ViewModel() { @@ -26,7 +25,6 @@ class CameraViewModel( private val _bitmap = MutableStateFlow(null) val bitmap: StateFlow = _bitmap - val prompt = AppConstant().prompt fun takePicture( controller: LifecycleCameraController, diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/component/CurtomCameraShutter.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/component/CurtomCameraShutter.kt new file mode 100644 index 0000000..7b9a977 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/camera/component/CurtomCameraShutter.kt @@ -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() +} \ No newline at end of file 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 new file mode 100644 index 0000000..9ae3ade --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/DetailScreen.kt @@ -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() + } +} + diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/PlantDiagnosisViewModel.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/PlantDiagnosisViewModel.kt new file mode 100644 index 0000000..bfe08a1 --- /dev/null +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/detail/PlantDiagnosisViewModel.kt @@ -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?>(null) + val state: StateFlow?> = _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) + } + } + } +} \ No newline at end of file