Compare commits

...

10 Commits

32 changed files with 5850 additions and 120 deletions

View File

@ -105,4 +105,7 @@ dependencies {
// placeholder or shimmer loading // placeholder or shimmer loading
implementation(libs.accompanist.placeholder.material) implementation(libs.accompanist.placeholder.material)
// Lottie Animation
implementation(libs.lottie.compose)
} }

View File

@ -0,0 +1,19 @@
package com.syaroful.agrilinkvocpro.core.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.syaroful.agrilinkvocpro.R
@Composable
fun Loader() {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading))
LottieAnimation(
composition,
modifier = Modifier.fillMaxWidth(0.5f)
)
}

View File

@ -18,6 +18,8 @@ fun getValuesForSensorNpk(sensor: String, data: List<NpkWithHour>): List<Double>
} }
} }
fun getValuesForSensorDht(sensor: String, data: List<DhtWithHour>): List<Double> { fun getValuesForSensorDht(sensor: String, data: List<DhtWithHour>): List<Double> {
return when (sensor) { return when (sensor) {
"Kelembaban Udara" -> data.mapNotNull { it.viciHumidity?.toDouble() } "Kelembaban Udara" -> data.mapNotNull { it.viciHumidity?.toDouble() }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,6 @@ converterGson = "2.9.0"
datastorePreferences = "1.1.7" datastorePreferences = "1.1.7"
featureDelivery = "2.1.0" featureDelivery = "2.1.0"
firebaseBom = "33.13.0" firebaseBom = "33.13.0"
hiltAndroid = "2.56.2"
hiltAndroidCompiler = "2.56.2"
hiltNavigationCompose = "1.2.0"
javaJwt = "4.4.0" javaJwt = "4.4.0"
koinAndroid = "3.5.3" koinAndroid = "3.5.3"
koinAndroidxCompose = "3.5.3" koinAndroidxCompose = "3.5.3"
@ -17,17 +14,13 @@ coreKtx = "1.16.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
kotlinxSerializationJson = "1.6.3"
kotlinxSerializationJsonVersion = "1.6.0" kotlinxSerializationJsonVersion = "1.6.0"
ktorClientCio = "2.3.3"
ktorClientContentNegotiation = "2.3.3"
ktorClientCore = "2.3.3"
ktorSerializationKotlinxJson = "2.3.3"
lifecycleRuntimeKtx = "2.9.0" lifecycleRuntimeKtx = "2.9.0"
activityCompose = "1.10.1" activityCompose = "1.10.1"
composeBom = "2025.05.00" composeBom = "2025.05.00"
lifecycleViewmodelCompose = "2.9.0" lifecycleViewmodelCompose = "2.9.0"
loggingInterceptor = "4.11.0" loggingInterceptor = "4.11.0"
lottieCompose = "6.6.6"
navigationCompose = "2.9.0" navigationCompose = "2.9.0"
okhttp = "4.12.0" okhttp = "4.12.0"
retrofit = "2.9.0" retrofit = "2.9.0"
@ -35,7 +28,6 @@ retrofit2KotlinxSerializationConverter = "0.8.0"
roomRuntime = "2.7.1" roomRuntime = "2.7.1"
runtime = "1.8.2" runtime = "1.8.2"
runtimeAndroid = "1.8.1" runtimeAndroid = "1.8.1"
ycharts = "2.1.0"
[libraries] [libraries]
accompanist-placeholder-material = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanistSwiperefresh" } accompanist-placeholder-material = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanistSwiperefresh" }
@ -46,7 +38,6 @@ androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", versi
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" } androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
@ -75,17 +66,12 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidxCompose" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidxCompose" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
kotlinx-serialization-json-v160 = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJsonVersion" } kotlinx-serialization-json-v160 = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJsonVersion" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCio" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClientContentNegotiation" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorSerializationKotlinxJson" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" } retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
ycharts = { module = "co.yml:ycharts", version.ref = "ycharts" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }

View File

@ -5,7 +5,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import com.syaroful.agrilinkvocpro.growth_recipe_feature.di.appModule import com.syaroful.agrilinkvocpro.growth_recipe_feature.di.appModule
import com.syaroful.agrilinkvocpro.growth_recipe_feature.di.clusterNetworkModule
import com.syaroful.agrilinkvocpro.growth_recipe_feature.di.networkModule import com.syaroful.agrilinkvocpro.growth_recipe_feature.di.networkModule
import com.syaroful.agrilinkvocpro.growth_recipe_feature.di.viewModelModule import com.syaroful.agrilinkvocpro.growth_recipe_feature.di.viewModelModule
import com.syaroful.agrilinkvocpro.growth_recipe_feature.naviagtion.SetupNavigation import com.syaroful.agrilinkvocpro.growth_recipe_feature.naviagtion.SetupNavigation
@ -17,18 +16,17 @@ class GrowthRecipeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
loadKoinModules(listOf(appModule, networkModule, clusterNetworkModule, viewModelModule)) loadKoinModules(listOf(appModule, networkModule, viewModelModule))
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
AgrilinkVocproTheme { AgrilinkVocproTheme {
SetupNavigation() SetupNavigation()
} }
} }
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unloadKoinModules(listOf(appModule, networkModule, clusterNetworkModule, viewModelModule)) unloadKoinModules(listOf(appModule, networkModule, viewModelModule))
} }
} }

View File

@ -0,0 +1,16 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.core.utils
import com.syaroful.agrilinkvocpro.growth_recipe_feature.data.model.NpkWithDay
fun getValuesForSensorNpk(sensor: String, data: List<NpkWithDay>): List<Double> {
return when (sensor) {
"Nitrogen" -> data.mapNotNull { it.soilNitrogen?.toDouble() }
"Pospor" -> data.mapNotNull { it.soilPhosphorus?.toDouble() }
"Kalium" -> data.mapNotNull { it.soilPotassium?.toDouble() }
"Suhu Tanah" -> data.mapNotNull { it.soilTemperature?.toDouble() }
"PH Tanah" -> data.mapNotNull { it.soilPh?.toDouble() }
"Kelembapan" -> data.mapNotNull { it.soilHumidity?.toDouble() }
"Konduktivitas" -> data.mapNotNull { it.soilConductivity?.toDouble() }
else -> emptyList()
}
}

View File

@ -1,26 +0,0 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.di
import com.syaroful.agrilinkvocpro.growth_recipe_feature.data.network.ClusteringService
import okhttp3.OkHttpClient
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
val clusterNetworkModule = module {
single {
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build()
}
single {
Retrofit.Builder()
.baseUrl("http://labai.polinema.ac.id:5050/")
.client(get())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single<ClusteringService> { get<Retrofit>().create(ClusteringService::class.java) }
}

View File

@ -1,26 +1,57 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.di package com.syaroful.agrilinkvocpro.growth_recipe_feature.di
import com.syaroful.agrilinkvocpro.growth_recipe_feature.data.network.ClusteringService
import com.syaroful.agrilinkvocpro.growth_recipe_feature.data.network.GrowthRecipeService import com.syaroful.agrilinkvocpro.growth_recipe_feature.data.network.GrowthRecipeService
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
val CLUSTER_CLIENT = named("CLUSTER_CLIENT")
val CLUSTER_RETROFIT = named("CLUSTER_RETROFIT")
val RECIPE_CLIENT = named("RECIPE_CLIENT")
val RECIPE_RETROFIT = named("RECIPE_RETROFIT")
val networkModule = module { val networkModule = module {
single {
// Cluster client and retrofit
single(CLUSTER_CLIENT) {
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build()
}
single(CLUSTER_RETROFIT) {
Retrofit.Builder()
.baseUrl("http://labai.polinema.ac.id:5050/")
.client(get(CLUSTER_CLIENT))
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single<ClusteringService> {
get<Retrofit>(CLUSTER_RETROFIT).create(ClusteringService::class.java)
}
// Growth Recipe client and retrofit
single(RECIPE_CLIENT) {
OkHttpClient.Builder() OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS) .connectTimeout(2, TimeUnit.SECONDS)
.build() .build()
} }
single { single(RECIPE_RETROFIT) {
Retrofit.Builder() Retrofit.Builder()
.baseUrl("http://labai.polinema.ac.id:3042/") .baseUrl("http://labai.polinema.ac.id:3042/")
.client(get()) .client(get(RECIPE_CLIENT))
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
} }
single<GrowthRecipeService> { get<Retrofit>().create(GrowthRecipeService::class.java) } single<GrowthRecipeService> {
get<Retrofit>(RECIPE_RETROFIT).create(GrowthRecipeService::class.java)
}
} }

View File

@ -0,0 +1,11 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.di
import com.syaroful.agrilinkvocpro.growth_recipe_feature.presentation.recipe.GrowthRecipeViewModel
import com.syaroful.agrilinkvocpro.growth_recipe_feature.presentation.recomendation.ClusterViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModule = module {
viewModel { GrowthRecipeViewModel(get(), get()) }
viewModel { ClusterViewModel(get()) }
}

View File

@ -137,7 +137,6 @@ fun GrowthRecipeScreen(
} }
when (graphicState) { when (graphicState) {
is ResultState.Loading -> { is ResultState.Loading -> {
isRefreshing.value = true
Row { Row {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@ -18,6 +18,7 @@ import java.util.Date
private const val TAG = "GrowthRecipeViewModel" private const val TAG = "GrowthRecipeViewModel"
class GrowthRecipeViewModel( class GrowthRecipeViewModel(
private val userPreferences: UserPreferences, private val userPreferences: UserPreferences,
private val growthRecipeRepository: GraphicDataRepository private val growthRecipeRepository: GraphicDataRepository
@ -25,7 +26,8 @@ class GrowthRecipeViewModel(
private val _getGraphicState = private val _getGraphicState =
MutableStateFlow<ResultState<NpkGraphicDayResponse>>(ResultState.Idle) MutableStateFlow<ResultState<NpkGraphicDayResponse>>(ResultState.Idle)
val getGraphicState: StateFlow<ResultState<NpkGraphicDayResponse>> = _getGraphicState.asStateFlow() val getGraphicState: StateFlow<ResultState<NpkGraphicDayResponse>> =
_getGraphicState.asStateFlow()
private val today = Date() private val today = Date()
private val tenDaysAgo = Date(today.time - 9 * 24 * 60 * 60 * 1000) private val tenDaysAgo = Date(today.time - 9 * 24 * 60 * 60 * 1000)

View File

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -34,10 +33,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.core.components.Loader
import com.syaroful.agrilinkvocpro.core.utils.ResultState import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.growth_recipe_feature.R import com.syaroful.agrilinkvocpro.growth_recipe_feature.R
import com.syaroful.agrilinkvocpro.growth_recipe_feature.core.component.ListItemClustering import com.syaroful.agrilinkvocpro.growth_recipe_feature.core.component.ListItemClustering
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -98,9 +97,7 @@ fun ClusterScreen(
) { ) {
when (state) { when (state) {
is ResultState.Loading -> { is ResultState.Loading -> {
CircularProgressIndicator( Loader()
color = MainGreen
)
} }
is ResultState.Error -> { is ResultState.Error -> {

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

View File

@ -0,0 +1,53 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.component
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.R
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
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",
)
}
}
}

View File

@ -0,0 +1,8 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.PlantDiseaseDetectionResponse
sealed class DiagnosisResult {
data class Success(val data: PlantDiseaseDetectionResponse) : DiagnosisResult()
data class Error(val errorMessage: String) : DiagnosisResult()
}

View File

@ -0,0 +1,6 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model
@kotlinx.serialization.Serializable
data class ErrorResponse(
val error: String? = null
)

View File

@ -1,7 +1,7 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.PlantDiseaseDetectionResponse import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.DiagnosisResult
interface PlantDiagnosisRepository { interface PlantDiagnosisRepository {
suspend fun detectDisease(base64Image: String, prompt: String): PlantDiseaseDetectionResponse suspend fun detectDisease(base64Image: String, prompt: String): DiagnosisResult
} }

View File

@ -1,6 +1,7 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository
import android.util.Log import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.DiagnosisResult
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.ErrorResponse
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiContent 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.GeminiPart
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiRequest import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.model.GeminiRequest
@ -10,32 +11,44 @@ import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.network.
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class PlantDiagnosisRepositoryImpl(private val api: GeminiApiService) : PlantDiagnosisRepository { class PlantDiagnosisRepositoryImpl(private val api: GeminiApiService) : PlantDiagnosisRepository {
private val json = Json { ignoreUnknownKeys = true }
override suspend fun detectDisease( override suspend fun detectDisease(
base64Image: String, base64Image: String,
prompt: String prompt: String
): PlantDiseaseDetectionResponse { ): DiagnosisResult {
val request = GeminiRequest( return try {
contents = listOf( val request = GeminiRequest(
GeminiContent( contents = listOf(
parts = listOf( GeminiContent(
GeminiPart(text = prompt), parts = listOf(
GeminiPart(inline_data = InlineData("image/jpeg", base64Image)) GeminiPart(text = prompt),
GeminiPart(inline_data = InlineData("image/jpeg", base64Image))
)
) )
) )
) )
) val response = api.detectDisease(
apiKey = "AIzaSyDYZHlMfOdFAcTFptWHqhPwIO734VdNUWE",
request = request
)
val response = api.detectDisease( val rawJson = response.candidates.first().content.parts.first().text
apiKey = "AIzaSyDYZHlMfOdFAcTFptWHqhPwIO734VdNUWE", // Ideally use BuildConfig or secure storage val cleanedJson = rawJson
request = request .replace("```json", "")
) .replace("```", "")
Log.d("PlantDiagnosisRepository", "Response: $response") .trim()
try {
val rawJson = response.candidates.first().content.parts.first().text val parsedSuccess =
val cleanedJson = rawJson json.decodeFromString<PlantDiseaseDetectionResponse>(cleanedJson)
.replace("```json", "") DiagnosisResult.Success(parsedSuccess)
.replace("```", "") } catch (e: Exception) {
.trim() val parsedError = json.decodeFromString<ErrorResponse>(cleanedJson)
return Json.decodeFromString(cleanedJson) DiagnosisResult.Error(parsedError.error.toString())
}
} catch (e: Exception) {
DiagnosisResult.Error("Terjadi kesalahan: ${e.localizedMessage ?: "tidak diketahui"}")
}
} }
} }

View File

@ -17,7 +17,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -41,9 +40,9 @@ 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.AppButton
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.core.components.Loader
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
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -119,7 +118,7 @@ fun DetailScreen(
Box( Box(
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) { ) {
CircularProgressIndicator(color = MainGreen) Loader()
} }
} }
@ -221,7 +220,11 @@ fun DetailScreen(
} }
Spacer(modifier = Modifier.height(56.dp)) Spacer(modifier = Modifier.height(56.dp))
} else { } else {
Text("Gagal parsing response") DefaultErrorComponent(
painter = painterResource(id = R.drawable.mascot_surprised),
label = "Keknya bukan gambar daun",
message = "Gambar tidak menunjukkan daun tanaman, mohon unggah gambar daun yang jelas"
)
} }
} }
} }

View File

@ -4,6 +4,7 @@ import android.graphics.Bitmap
import android.util.Log 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.DiagnosisResult
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.core.extention.toByteArray
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
@ -30,9 +31,18 @@ class PlantDiagnosisViewModel(
viewModelScope.launch { viewModelScope.launch {
try { try {
val base64 = bitmap.toBase64() val base64 = bitmap.toBase64()
val result = repository.detectDisease(prompt = prompt, base64Image = base64) when (val result =
_state.value = Result.success(result) repository.detectDisease(prompt = prompt, base64Image = base64)) {
Log.d("PlantDiagnosisViewModel", "Diagnosis result: $result") is DiagnosisResult.Success -> {
_state.value = Result.success(result.data)
Log.d("PlantDiagnosisViewModel", "Diagnosis result: ${result.data}")
}
is DiagnosisResult.Error -> {
_state.value = Result.failure(Exception(result.errorMessage))
Log.e("PlantDiagnosisViewModel", "Diagnosis error: ${result.errorMessage}")
}
}
} catch (e: Exception) { } catch (e: Exception) {
_state.value = Result.failure(e) _state.value = Result.failure(e)
Log.e("PlantDiagnosisViewModel", "Error during diagnosis", e) Log.e("PlantDiagnosisViewModel", "Error during diagnosis", e)

View File

@ -27,7 +27,6 @@ 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.Color
@ -40,6 +39,7 @@ 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.component.DiseaseDetectionBanner
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.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
@ -185,34 +185,4 @@ private fun DiagnosisListItem(
} }
} }
@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",
)
}
}
}