feat: refactor package

This commit is contained in:
Cutiful 2025-06-13 13:47:25 +07:00
parent 6b5010d37d
commit b0a3130cb9
41 changed files with 674 additions and 296 deletions

View File

@ -102,7 +102,7 @@ dependencies {
// pull to refresh
implementation(libs.accompanist.swiperefresh)
//graphic chat by Ycharts
implementation(libs.ycharts)
// placeholder or shimmer loading
implementation(libs.accompanist.placeholder.material)
}

View File

@ -8,8 +8,8 @@ import androidx.activity.enableEdgeToEdge
import com.syaroful.agrilinkvocpro.core.components.DownloadModuleConfirmationDialog
import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.navigation.SetupNavigation
import com.syaroful.agrilinkvocpro.ui.screen.home.DynamicModuleViewModel
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.presentation.screen.home.DynamicModuleViewModel
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel

View File

@ -16,7 +16,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable

View File

@ -26,9 +26,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.DarkGrey
import com.syaroful.agrilinkvocpro.presentation.theme.LightGrey
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable

View File

@ -21,8 +21,8 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.LightGrey
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@OptIn(ExperimentalFoundationApi::class)

View File

@ -14,7 +14,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun DownloadProgressDialog(

View File

@ -22,7 +22,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun MenuItemButton(

View File

@ -4,7 +4,7 @@ import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
import com.syaroful.agrilinkvocpro.presentation.theme.LightGrey
val textTheme = Typography(
displayLarge = TextStyle(

View File

@ -0,0 +1,61 @@
package com.syaroful.agrilinkvocpro.core.placeholder
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
fun Modifier.shimmerEffect(
shimmerColors: List<Color> = listOf(
Color.LightGray.copy(alpha = 0.6f),
Color.LightGray.copy(alpha = 0.2f),
Color.LightGray.copy(alpha = 0.6f)
),
shimmerDuration: Int = 1000,
cornerRadius: Dp = 16.dp
): Modifier = composed {
var size by remember { mutableStateOf(IntSize.Zero) }
val transition = rememberInfiniteTransition()
val xShimmer by transition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = shimmerDuration, easing = LinearEasing)
)
)
val brush = Brush.linearGradient(
colors = shimmerColors,
start = Offset.Zero,
end = Offset(
x = size.width * xShimmer,
y = size.height * xShimmer
)
)
this
.onGloballyPositioned { coordinates ->
size = coordinates.size
}
.clip(RoundedCornerShape(cornerRadius))
.background(brush)
}

View File

@ -0,0 +1,10 @@
package com.syaroful.agrilinkvocpro.core.utils.extention
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun Date.toFormattedString(): String {
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
return sdf.format(this)
}

View File

@ -0,0 +1,19 @@
package com.syaroful.agrilinkvocpro.core.utils.extention
fun mapToUserFriendlyError(e: Exception): String {
return when (e) {
is java.net.UnknownHostException -> "Tidak dapat terhubung ke server. Periksa koneksi internet Anda."
is java.net.SocketTimeoutException -> "Waktu koneksi habis. Mohon coba lagi."
is java.io.IOException -> "Terjadi kesalahan jaringan. Silakan cek koneksi Anda."
is retrofit2.HttpException -> {
when (e.code()) {
401 -> "Akses ditolak. Silakan logout dan login kembali."
403 -> "Anda tidak memiliki izin untuk mengakses ini."
404 -> "Data tidak ditemukan."
500 -> "Terjadi kesalahan pada server. Silakan coba lagi nanti."
else -> "Terjadi kesalahan. Kode: ${e.code()}"
}
}
else -> "Terjadi kesalahan. Silakan coba lagi."
}
}

View File

@ -1,25 +1,11 @@
package com.syaroful.agrilinkvocpro.data.model
data class Npk1GraphicDataResponse(
val data: ListDataNpk1?,
data class NpkGraphicDataResponse(
val data: Map<String, List<NpkWithHour>>?,
val statusCode: Int?,
val message: String?,
)
data class ListDataNpk1(
val npk1: List<NpkWithHour>?,
)
data class Npk2GraphicDataResponse(
val data: ListDataNpk2?,
val statusCode: Int?,
val message: String?,
)
data class ListDataNpk2(
val npk1: List<NpkWithHour>?,
)
data class NpkWithHour(
val hour: Int?,
val date: String?,

View File

@ -1,12 +1,14 @@
package com.syaroful.agrilinkvocpro.data.network
import com.syaroful.agrilinkvocpro.data.model.LoginResponse
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
data class LoginRequest(
@ -25,4 +27,14 @@ interface ApiService {
suspend fun getLatestSensorData(
@Header("Authorization") authHeader: String
): Response<SensorDataResponse>
@GET("api/sensor/getData")
suspend fun getNpk1DataSensor(
@Header("Authorization") authHeader: String,
@Query("range[start]") startDate: String,
@Query("range[end]") endDate: String,
@Query("range[time_range]") timeRange: String = "HOURLY",
@Query("sensor") sensor: String
): Response<NpkGraphicDataResponse>
}

View File

@ -1,12 +1,30 @@
package com.syaroful.agrilinkvocpro.data.repository
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.network.ApiService
import retrofit2.Response
class SensorDataRepository(private val apiService: ApiService) {
private var _latestSensorData: SensorDataResponse? = null
val latestSensorData: SensorDataResponse?
get() = _latestSensorData
suspend fun getLatestSensorData(authHeader: String): Response<SensorDataResponse> {
return apiService.getLatestSensorData(authHeader)
val response = apiService.getLatestSensorData(authHeader)
if (response.isSuccessful) {
_latestSensorData = response.body()
}
return response
}
suspend fun getNpkDataSensor(
authHeader: String,
startDate: String,
endDate: String,
timeRange: String = "HOURLY",
sensor: String
): Response<NpkGraphicDataResponse> {
return apiService.getNpk1DataSensor(authHeader, startDate, endDate, timeRange, sensor)
}
}

View File

@ -1,9 +1,10 @@
package com.syaroful.agrilinkvocpro.di
import com.syaroful.agrilinkvocpro.ui.screen.home.DynamicModuleViewModel
import com.syaroful.agrilinkvocpro.ui.screen.home.HomeViewModel
import com.syaroful.agrilinkvocpro.ui.screen.login.LoginViewModel
import com.syaroful.agrilinkvocpro.ui.screen.profile.ProfileViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.detail.DetailViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.home.DynamicModuleViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.home.HomeViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.login.LoginViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.profile.ProfileViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
@ -12,4 +13,5 @@ val viewModelModule = module {
viewModel { LoginViewModel(get(), get()) }
viewModel { ProfileViewModel(get()) }
viewModel { HomeViewModel(get(), get()) }
viewModel { DetailViewModel(get(), get()) }
}

View File

@ -1,18 +1,20 @@
package com.syaroful.agrilinkvocpro.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.ui.screen.detail.DetailScreen
import com.syaroful.agrilinkvocpro.ui.screen.home.DynamicModuleViewModel
import com.syaroful.agrilinkvocpro.ui.screen.home.HomeScreen
import com.syaroful.agrilinkvocpro.ui.screen.login.LoginScreen
import com.syaroful.agrilinkvocpro.ui.screen.login.LoginViewModel
import com.syaroful.agrilinkvocpro.ui.screen.profile.ProfileScreen
import com.syaroful.agrilinkvocpro.ui.screen.register.RegisterScreen
import com.syaroful.agrilinkvocpro.ui.screen.splash.SplashScreen
import com.syaroful.agrilinkvocpro.presentation.screen.detail.DetailScreen
import com.syaroful.agrilinkvocpro.presentation.screen.home.DynamicModuleViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.home.HomeScreen
import com.syaroful.agrilinkvocpro.presentation.screen.login.LoginScreen
import com.syaroful.agrilinkvocpro.presentation.screen.login.LoginViewModel
import com.syaroful.agrilinkvocpro.presentation.screen.profile.ProfileScreen
import com.syaroful.agrilinkvocpro.presentation.screen.register.RegisterScreen
import com.syaroful.agrilinkvocpro.presentation.screen.splash.SplashScreen
import org.koin.androidx.compose.koinViewModel
@Composable
@ -83,8 +85,12 @@ fun SetupNavigation(
}
)
}
composable("detail-screen") {
DetailScreen()
composable(
route = "detail-screen/{sensorId}",
arguments = listOf(navArgument("sensorId") { type = NavType.StringType })
) { backStackEntry ->
val sensorId = backStackEntry.arguments?.getString("sensorId") ?: "npk1"
DetailScreen(sensorId = sensorId)
}
}
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.detail
package com.syaroful.agrilinkvocpro.presentation.screen.detail
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -25,7 +25,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun DataSensorBar(
@ -95,7 +95,7 @@ private fun CustomLinearProgressIndicator(
fun DataSensorBarPreview() {
DataSensorBar(
label = "Nitrogen",
percentage = 0.5f,
percentage = 20f,
painter = painterResource(id = R.drawable.npk),
)

View File

@ -0,0 +1,229 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
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.core.placeholder.shimmerEffect
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import org.koin.androidx.compose.koinViewModel
import java.text.SimpleDateFormat
import java.util.Date
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailScreen(
sensorId: String,
modifier: Modifier = Modifier,
detailViewModel: DetailViewModel = koinViewModel(),
) {
LaunchedEffect(sensorId) {
detailViewModel.fetchNpkData(sensorId)
}
val currentData = detailViewModel.currentSensorData
val sdf = SimpleDateFormat("dd MMMM yyyy", java.util.Locale.getDefault())
val currentDate = sdf.format(Date())
val options = listOf(
"Nitrogen",
"Pospor",
"Kalium",
"Suhu Tanah",
"PH Tanah",
"Kelembapan",
"Konduktivitas"
)
Scaffold(
topBar = {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
"Detail Grafik",
style = MaterialTheme.typography.titleMedium
)
}
},
)
},
) { innerPadding ->
val npkDataState by detailViewModel.npkDataState.collectAsState()
val isRefreshing = remember { mutableStateOf(false) }
PullToRefreshBox(
isRefreshing = isRefreshing.value,
onRefresh = {
isRefreshing.value = true
detailViewModel.fetchNpkData(sensorId)
},
) {
Column(
modifier = Modifier
.padding(innerPadding)
.padding(top = 16.dp)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
when (npkDataState) {
is ResultState.Loading -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.shimmerEffect()
)
}
is ResultState.Error -> {
isRefreshing.value = false
DefaultErrorComponent(
modifier = Modifier.padding(vertical = 20.dp),
label = "Oops!",
message = (npkDataState as ResultState.Error).message,
painter = painterResource(id = R.drawable.mascot_confused)
)
}
ResultState.Idle -> {
}
is ResultState.Success -> {
isRefreshing.value = false
val selectedSensor = remember { mutableStateOf(options[0]) }
val result = (npkDataState as ResultState.Success).data
val npkData = result?.data?.get(sensorId)
val hours = npkData?.filter { it.hour != null }?.map { it.hour!!.toInt() }
?: emptyList()
val values = when (selectedSensor.value) {
"Nitrogen" -> npkData?.mapNotNull { it.soilnitrogen?.toDouble() } ?: emptyList()
"Pospor" -> npkData?.mapNotNull { it.soilphosphorus?.toDouble() } ?: emptyList()
"Kalium" -> npkData?.mapNotNull { it.soilpotassium?.toDouble() } ?: emptyList()
"Suhu Tanah" -> npkData?.mapNotNull { it.soiltemperature?.toDouble() } ?: emptyList()
"PH Tanah" -> npkData?.mapNotNull { it.soilph?.toDouble() } ?: emptyList()
"Kelembapan" -> npkData?.mapNotNull { it.soilhumidity?.toDouble() } ?: emptyList()
"Konduktivitas" -> npkData?.mapNotNull { it.soilconductivity?.toDouble() } ?: emptyList()
else -> emptyList()
}
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
}
Spacer(modifier = Modifier.height(16.dp))
LineChart(
hours = hours,
values = values,
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.align(CenterHorizontally)
)
}
}
Text(
"Grafik ini adalah grafik per hari ini tanggal $currentDate",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic,
modifier = Modifier
.fillMaxWidth()
.alpha(0.5f)
.padding(vertical = 8.dp)
)
if (currentData != null) {
val dataSensor = when (sensorId) {
"npk1" -> currentData.data?.npk1
"npk2" -> currentData.data?.npk2
else -> null
}
Column(
verticalArrangement = Arrangement.spacedBy(
8.dp,
Alignment.Top
)
) {
DataSensorBar(
label = "Nitrogen",
value = dataSensor?.soilnitrogen ?: 0,
painter = painterResource(id = R.drawable.npk), // Assuming the max value is 255
percentage = (dataSensor?.soilnitrogen?.toFloat() ?: 0f) / 50f
)
DataSensorBar(
label = "Pospor",
value = dataSensor?.soilphosphorus ?: 0,
painter = painterResource(id = R.drawable.npk), // Assuming the max value is 255
percentage = (dataSensor?.soilphosphorus?.toFloat() ?: 0f) / 255f
)
DataSensorBar(
label = "Kalium",
value = dataSensor?.soilpotassium ?: 0,
painter = painterResource(id = R.drawable.npk), // Assuming the max value is 255
percentage = (dataSensor?.soilpotassium?.toFloat() ?: 0f) / 255f
)
DataSensorBar(
label = "Kelembaban",
value = dataSensor?.soilhumidity ?: 0,
painter = painterResource(id = R.drawable.soil_humidity), // Assuming the max value is 100
percentage = (dataSensor?.soilhumidity?.toFloat() ?: 0f) / 100f
)
DataSensorBar(
label = "Suhu Tanah",
value = dataSensor?.soiltemperature ?: 0,
painter = painterResource(id = R.drawable.soil_temperature), // Assuming the max value is 50 (adjust as needed)
percentage = (dataSensor?.soiltemperature?.toFloat() ?: 0f) / 50f
)
DataSensorBar(
label = "PH Tanah",
value = dataSensor?.soilph ?: 0,
painter = painterResource(id = R.drawable.meters), // Assuming the max value is 14
percentage = (dataSensor?.soilph?.toFloat() ?: 0f) / 14f
)
DataSensorBar(
label = "Konduktivitas",
value = dataSensor?.soilconductivity ?: 0,
painter = painterResource(id = R.drawable.electricity),
percentage = (dataSensor?.soilconductivity?.toFloat() ?: 0f) / 200f
)
}
} else {
Text("data Sesnor kosong")
}
}
}
}
}

View File

@ -0,0 +1,70 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.repository.SensorDataRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.util.Date
private const val TAG = "DetailViewModel"
class DetailViewModel(
private val sensorDataRepository: SensorDataRepository,
private val userPreferences: UserPreferences
) : ViewModel() {
val currentSensorData: SensorDataResponse?
get() = sensorDataRepository.latestSensorData
private val _npkDataState = MutableStateFlow<ResultState<NpkGraphicDataResponse>>(ResultState.Idle)
val npkDataState: StateFlow<ResultState<NpkGraphicDataResponse>> = _npkDataState.asStateFlow()
private val today = Date()
fun fetchNpkData(
sensor: String,
) {
viewModelScope.launch {
_npkDataState.value = ResultState.Loading
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
val formattedToday = today.toFormattedString()
try {
delay(2000L)
val response = sensorDataRepository.getNpkDataSensor(
authHeader = authHeader,
startDate = formattedToday,
endDate = formattedToday,
timeRange = "HOURLY",
sensor = sensor
)
if (response.isSuccessful) {
response.body()?.let { body ->
_npkDataState.value = ResultState.Success(body)
} ?: run {
_npkDataState.value = ResultState.Error("Data tidak ditemukan")
}
} else {
_npkDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}")
}
} catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)
_npkDataState.value = ResultState.Error(errorMessage)
Log.d(TAG, "Failed to fetch data: ${e.message}")
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.detail
package com.syaroful.agrilinkvocpro.presentation.screen.detail
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.detail
package com.syaroful.agrilinkvocpro.presentation.screen.detail
import android.graphics.Paint
import androidx.compose.foundation.Canvas
@ -22,7 +22,7 @@ import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import kotlin.math.round
import kotlin.math.roundToInt
@ -36,7 +36,7 @@ fun LineChart(
Box(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(8.dp))
.padding(top = 20.dp)
.padding(top = 24.dp)
.padding(8.dp)
) {
val spacing = 100f
@ -130,6 +130,21 @@ fun LineChart(
cap = StrokeCap.Round
)
)
val labelPadding = 12.dp.toPx()
drawContext.canvas.nativeCanvas.drawText(
"Jumlah",
spacing / 2f,
labelPadding - 80,
textPaint
)
drawContext.canvas.nativeCanvas.drawText(
"Jam",
size.width - labelPadding,
size.height - 4,
textPaint
)
}
}
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.home
package com.syaroful.agrilinkvocpro.presentation.screen.home
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel

View File

@ -1,7 +1,5 @@
package com.syaroful.agrilinkvocpro.ui.screen.home
package com.syaroful.agrilinkvocpro.presentation.screen.home
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -21,8 +19,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -31,12 +27,11 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -44,22 +39,19 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.core.components.DownloadProgressDialog
import com.syaroful.agrilinkvocpro.core.components.MenuItemButton
import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.presentation.theme.LightGrey
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel
import kotlin.math.log
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -76,6 +68,10 @@ fun HomeScreen(
val homeState by homeViewModel.homeState.collectAsState()
val isRefreshing = remember { mutableStateOf(false) }
val sensors = listOf("NPK", "DHT")
var selectedSensor by remember { mutableStateOf("NPK") }
AgrilinkVocproTheme {
Scaffold { padding ->
PullToRefreshBox(
@ -98,20 +94,26 @@ fun HomeScreen(
onFeatureClick = onFeatureClick
)
Spacer(modifier = Modifier.height(32.dp))
Text(
modifier = Modifier
.padding(horizontal = 20.dp)
.fillMaxWidth(),
text = "Data Sensor Green House",
textAlign = TextAlign.Center,
style = textTheme.bodyMedium
SensorTabBar(
sensors = sensors,
selectedSensor = selectedSensor,
onSensorSelected = { selectedSensor = it }
)
when (homeState) {
is ResultState.Loading -> {
CircularProgressIndicator(
Box(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterHorizontally)
.fillMaxWidth()
.height(100.dp)
.shimmerEffect()
)
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(100.dp)
.shimmerEffect()
)
}
@ -119,28 +121,44 @@ fun HomeScreen(
isRefreshing.value = false
val data = (homeState as ResultState.Success).data
data?.data?.let { sensorData ->
BetDataComponent(
onClick = {
navController.navigate("detail-screen")
},
betNUmber = 1,
commodity = "Kabocha",
nitrogenValue = sensorData.npk1?.soilnitrogen.toString(),
phosphorValue = sensorData.npk1?.soilphosphorus.toString(),
potassiumValue = sensorData.npk1?.soilpotassium.toString(),
commodityImage = painterResource(id = R.drawable.kabocha)
)
BetDataComponent(
onClick = {
navController.navigate("detail-screen")
},
betNUmber = 2,
commodity = "Melon",
nitrogenValue = sensorData.npk2?.soilnitrogen.toString(),
phosphorValue = sensorData.npk2?.soilphosphorus.toString(),
potassiumValue = sensorData.npk2?.soilpotassium.toString(),
commodityImage = painterResource(id = R.drawable.melon)
)
when (selectedSensor) {
"NPK" -> {
BetDataComponent(
onClick = {
navController.navigate("detail-screen/npk1")
},
betNUmber = 1,
commodity = "Kabocha",
nitrogenValue = sensorData.npk1?.soilnitrogen.toString(),
phosphorValue = sensorData.npk1?.soilphosphorus.toString(),
potassiumValue = sensorData.npk1?.soilpotassium.toString(),
commodityImage = painterResource(id = R.drawable.kabocha)
)
BetDataComponent(
onClick = {
navController.navigate("detail-screen/npk2")
},
betNUmber = 2,
commodity = "Melon",
nitrogenValue = sensorData.npk2?.soilnitrogen.toString(),
phosphorValue = sensorData.npk2?.soilphosphorus.toString(),
potassiumValue = sensorData.npk2?.soilpotassium.toString(),
commodityImage = painterResource(id = R.drawable.melon)
)
}
"DHT" -> {
DhtDataComponent(
onClick = {
},
temperatureValue = sensorData.dht?.vicitemperature.toString(),
humidityValue = sensorData.dht?.vicihumidity.toString(),
luminosityValue = sensorData.dht?.viciluminosity.toString(),
)
}
}
}
}
@ -237,6 +255,50 @@ private fun BetDataComponent(
}
}
@Composable
private fun DhtDataComponent(
onClick: () -> Unit,
temperatureValue: String,
humidityValue: String,
luminosityValue: String,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 8.dp)
.clip(shape = RoundedCornerShape(16.dp))
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
)
.border(
width = 1.dp,
color = MainGreen.copy(alpha = 0.4f),
shape = RoundedCornerShape(16.dp)
)
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
{
SensorDataItem(
icon = "Temp",
label = "Suhu\nGreen House",
value = temperatureValue
)
SensorDataItem(
icon = "Hum",
label = "Kelembaban\nUdara",
value = humidityValue
)
SensorDataItem(
icon = "Lux",
label = "Intensitas\ncahaya",
value = luminosityValue
)
}
}
@Composable
private fun SensorDataItem(
icon: String = "-",
@ -245,7 +307,7 @@ private fun SensorDataItem(
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = icon, color = MainGreen, style = textTheme.headlineMedium)
Text(text = label, color = LightGrey, style = textTheme.bodySmall)
Text(text = label, color = LightGrey, style = textTheme.bodySmall, textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(8.dp))
Text(text = value, style = textTheme.headlineSmall)
}

View File

@ -1,12 +1,14 @@
package com.syaroful.agrilinkvocpro.ui.screen.home
package com.syaroful.agrilinkvocpro.presentation.screen.home
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.repository.SensorDataRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
@ -22,6 +24,9 @@ class HomeViewModel(
private val _homeState = MutableStateFlow<ResultState<SensorDataResponse>>(ResultState.Idle)
val homeState: StateFlow<ResultState<SensorDataResponse>> = _homeState
// var currentDataSensor: SensorDataResponse? = null
// private set
init {
getGreenHouseData()
}
@ -38,9 +43,12 @@ class HomeViewModel(
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
try {
delay(1000L)
val response = sensorDataRepository.getLatestSensorData(authHeader)
if (response.isSuccessful) {
_homeState.value = ResultState.Success(response.body())
val responseBody = response.body()
_homeState.value = ResultState.Success(responseBody)
// currentDataSensor = responseBody
Log.d(TAG, "Success to fetch data: ${response.body()} \nCurl response: ${response.raw()}")
} else {
val errorBody = response.errorBody()?.string()
@ -50,7 +58,6 @@ class HomeViewModel(
.optString("message", "Failed to fetch data: ${response.code()}")
} ?: "Failed to fetch data: ${response.code()}"
_homeState.value = ResultState.Error(errorMessage)
// Log.d(TAG, errorMessage) // Sudah di-log di atas
}
} catch (e: Exception){
val errorMessage = mapToUserFriendlyError(e)
@ -59,21 +66,4 @@ class HomeViewModel(
}
}
}
private fun mapToUserFriendlyError(e: Exception): String {
return when (e) {
is java.net.UnknownHostException -> "Tidak dapat terhubung ke server. Periksa koneksi internet Anda."
is java.net.SocketTimeoutException -> "Waktu koneksi habis. Mohon coba lagi."
is java.io.IOException -> "Terjadi kesalahan jaringan. Silakan cek koneksi Anda."
is retrofit2.HttpException -> {
when (e.code()) {
401 -> "Akses ditolak. Silakan logout dan login kembali."
403 -> "Anda tidak memiliki izin untuk mengakses ini."
404 -> "Data tidak ditemukan."
500 -> "Terjadi kesalahan pada server. Silakan coba lagi nanti."
else -> "Terjadi kesalahan. Kode: ${e.code()}"
}
}
else -> "Terjadi kesalahan. Silakan coba lagi."
}
}
}

View File

@ -0,0 +1,52 @@
package com.syaroful.agrilinkvocpro.presentation.screen.home
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun SensorTabBar(
sensors: List<String>,
selectedSensor: String,
onSensorSelected: (String) -> Unit,
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
) {
sensors.forEach { sensor ->
val isSelected = sensor == selectedSensor
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.clip(RoundedCornerShape(50))
.background(if (isSelected) MainGreen else MaterialTheme.colorScheme.surfaceContainer)
.clickable { onSensorSelected(sensor) }
.padding(horizontal = 24.dp, vertical = 8.dp)
) {
Text(
text = sensor,
color = if (isSelected) Color.White else MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium
)
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.login
package com.syaroful.agrilinkvocpro.presentation.screen.login
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.Image
@ -36,9 +36,9 @@ import com.syaroful.agrilinkvocpro.core.components.AppPasswordField
import com.syaroful.agrilinkvocpro.core.components.AppTextField
import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.presentation.theme.DarkGrey
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.login
package com.syaroful.agrilinkvocpro.presentation.screen.login
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.profile
package com.syaroful.agrilinkvocpro.presentation.screen.profile
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
@ -38,8 +38,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.LightGrey
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.profile
package com.syaroful.agrilinkvocpro.presentation.screen.profile
import android.util.Log
import androidx.lifecycle.ViewModel

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.register
package com.syaroful.agrilinkvocpro.presentation.screen.register
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
@ -29,8 +29,8 @@ import com.syaroful.agrilinkvocpro.core.components.AppButton
import com.syaroful.agrilinkvocpro.core.components.AppPasswordField
import com.syaroful.agrilinkvocpro.core.components.AppTextField
import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.DarkGrey
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun RegisterScreen(

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.screen.splash
package com.syaroful.agrilinkvocpro.presentation.screen.splash
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -20,8 +20,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.ui.theme.DarkGreen
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
import com.syaroful.agrilinkvocpro.presentation.theme.DarkGreen
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import kotlinx.coroutines.delay
@Composable

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.theme
package com.syaroful.agrilinkvocpro.presentation.theme
import androidx.compose.ui.graphics.Color

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.theme
package com.syaroful.agrilinkvocpro.presentation.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.ui.theme
package com.syaroful.agrilinkvocpro.presentation.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle

View File

@ -1,155 +0,0 @@
package com.syaroful.agrilinkvocpro.ui.screen.detail
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
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.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.random.Random
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailScreen(
modifier: Modifier = Modifier
) {
val sdf = SimpleDateFormat("dd MMMM yyyy", java.util.Locale.getDefault())
val currentDate = sdf.format(Date())
val options = listOf(
"Nitrogen",
"Pospor",
"Kalium",
"Suhu Tanah",
"PH Tanah",
"Kelembapan",
"Konduktivitas"
)
Scaffold(
topBar = {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
"Detail Grafik",
style = MaterialTheme.typography.titleMedium
)
}
},
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(top = 16.dp)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
DynamicBottomSheet(
options = options
) {
}
Spacer(modifier = Modifier.height(16.dp))
LineChart(
hours = listOf(0, 1, 2, 3, 5, 6, 8, 10),
values = listOf(20.0, 22.0, 25.0, 23.0, 28.0, 30.0, 20.0, 22.0),
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.align(CenterHorizontally)
)
Text(
"Grafik ini adalah grafik per hari ini tanggal $currentDate",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic,
modifier = Modifier
.fillMaxWidth()
.alpha(0.5f)
.padding(vertical = 8.dp)
)
Column(
verticalArrangement = Arrangement.spacedBy(
8.dp,
Alignment.Top
)
) {
DataSensorBar(
label = "Nitrogen",
value = Random.nextInt(0, 101),
painter = painterResource(id = R.drawable.npk),
percentage = 0.5f
)
DataSensorBar(
label = "Pospor",
value = Random.nextInt(0, 101),
painter = painterResource(id = R.drawable.npk),
percentage = 0.43f
)
DataSensorBar(
label = "Kalium",
value = Random.nextInt(0, 101),
painter = painterResource(id = R.drawable.npk),
percentage = 0.3f
)
DataSensorBar(
label = "Kelembaban",
value = Random.nextInt(0, 101),
painter = painterResource(id = R.drawable.soil_humidity),
percentage = 0.2f
)
DataSensorBar(
label = "Suhu Tanah",
value = Random.nextInt(0, 101),
painter = painterResource(id = R.drawable.soil_temperature),
percentage = 0.6f
)
DataSensorBar(
label = "PH Tanah",
value = Random.nextInt(0, 101),
painter = painterResource(id = R.drawable.meters),
percentage = 0.8f
)
DataSensorBar(
label = "Konduktivitas",
value = Random.nextInt(0, 101),
painter = painterResource(id = R.drawable.electricity),
percentage = 0.6f
)
}
}
}
}
@Preview
@Composable
fun PreviewDetailScreen() {
DetailScreen()
}

View File

@ -11,7 +11,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
class PricePredictionActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -37,6 +37,7 @@ runtimeAndroid = "1.8.1"
ycharts = "2.1.0"
[libraries]
accompanist-placeholder-material = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanistSwiperefresh" }
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanistSwiperefresh" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" }

View File

@ -11,7 +11,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
class GrowthRecipeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -13,7 +13,7 @@ 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 com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
import org.koin.core.context.loadKoinModules
import org.koin.core.context.unloadKoinModules

View File

@ -19,7 +19,7 @@ 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
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@Composable
fun CustomCameraShutter(

View File

@ -40,7 +40,7 @@ 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 com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)