Final
This commit is contained in:
parent
7cd246cea6
commit
97ef889d46
203
.idea/other.xml
203
.idea/other.xml
|
@ -3,6 +3,17 @@
|
|||
<component name="direct_access_persist.xml">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Sony" />
|
||||
<option name="codename" value="A402SO" />
|
||||
<option name="id" value="A402SO" />
|
||||
<option name="manufacturer" value="Sony" />
|
||||
<option name="name" value="Xperia 10" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2520" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
|
@ -14,6 +25,17 @@
|
|||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OnePlus" />
|
||||
<option name="codename" value="OP535DL1" />
|
||||
<option name="id" value="OP535DL1" />
|
||||
<option name="manufacturer" value="OnePlus" />
|
||||
<option name="name" value="CPH2409" />
|
||||
<option name="screenDensity" value="401" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2412" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OnePlus" />
|
||||
|
@ -49,14 +71,14 @@
|
|||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Lenovo" />
|
||||
<option name="codename" value="TB370FU" />
|
||||
<option name="id" value="TB370FU" />
|
||||
<option name="manufacturer" value="Lenovo" />
|
||||
<option name="name" value="Tab P12" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1840" />
|
||||
<option name="screenY" value="2944" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a14m" />
|
||||
<option name="id" value="a14m" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-A145R" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2408" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
|
@ -69,6 +91,17 @@
|
|||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a15x" />
|
||||
<option name="id" value="a15x" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="A15 5G" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
|
@ -91,17 +124,6 @@
|
|||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
|
@ -146,6 +168,17 @@
|
|||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b6q" />
|
||||
<option name="id" value="b6q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Flip 6" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2640" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
|
@ -168,6 +201,17 @@
|
|||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
|
@ -179,6 +223,17 @@
|
|||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
|
@ -212,6 +267,17 @@
|
|||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="dubai" />
|
||||
<option name="id" value="dubai" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 30" />
|
||||
<option name="screenDensity" value="405" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
|
@ -245,6 +311,17 @@
|
|||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="eqe" />
|
||||
<option name="id" value="eqe" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 50 pro" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1220" />
|
||||
<option name="screenY" value="2712" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
|
@ -289,6 +366,17 @@
|
|||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="fogos" />
|
||||
<option name="id" value="fogos" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="moto g34 5G" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
|
@ -311,6 +399,17 @@
|
|||
<option name="screenX" value="1200" />
|
||||
<option name="screenY" value="1920" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts7lwifi" />
|
||||
<option name="id" value="gts7lwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-T870" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
|
@ -355,6 +454,17 @@
|
|||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="2304" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts9wifi" />
|
||||
<option name="id" value="gts9wifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-X710" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
|
@ -410,6 +520,28 @@
|
|||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="lyriq" />
|
||||
<option name="id" value="lyriq" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 40" />
|
||||
<option name="screenDensity" value="400" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="manaus" />
|
||||
<option name="id" value="manaus" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="edge 40 neo" />
|
||||
<option name="screenDensity" value="400" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="motorola" />
|
||||
|
@ -443,6 +575,17 @@
|
|||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="pa3q" />
|
||||
<option name="id" value="pa3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S25 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3120" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
|
@ -543,6 +686,17 @@
|
|||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="35" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tegu" />
|
||||
<option name="id" value="tegu" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
|
@ -565,6 +719,17 @@
|
|||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="xcover7" />
|
||||
<option name="id" value="xcover7" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="SM-G556B" />
|
||||
<option name="screenDensity" value="450" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2408" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
@ -57,6 +57,7 @@ dependencies {
|
|||
implementation (libs.ohteepee)
|
||||
|
||||
implementation("io.coil-kt.coil3:coil-compose:3.0.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -7,11 +7,16 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.example.caloryapp.navigation.Navigation
|
||||
import com.example.caloryapp.navigation.NavigationScreen
|
||||
//import com.example.caloryapp.pages.NavBarScreen
|
||||
import com.example.caloryapp.ui.theme.CaloryAppTheme
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
|
@ -34,29 +39,7 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
setContent {
|
||||
CaloryAppTheme {
|
||||
// Surface(
|
||||
// modifier = Modifier.fillMaxSize(),
|
||||
// color = MaterialTheme.colorScheme.background
|
||||
// ) {
|
||||
// val viewModel = viewModel<FoodDetectionViewModel>()
|
||||
// ScreenTest(viewModel)
|
||||
// }
|
||||
// val context = LocalContext.current
|
||||
// var classificationResult by remember { mutableStateOf("Memuat...") }
|
||||
//
|
||||
// Box {
|
||||
// CameraPreview(onImageCaptured = { byteBuffer ->
|
||||
// val result = foodClassifier.classify(byteBuffer)
|
||||
// classificationResult = result
|
||||
//
|
||||
// Toast.makeText(context, "Hasil: $result", Toast.LENGTH_SHORT).show()
|
||||
// })
|
||||
// }
|
||||
Navigation()
|
||||
|
||||
// LoginScreen(navController = rememberNavController())
|
||||
// ProfileScreen(navController = rememberNavController())
|
||||
// MainScreen()
|
||||
Navigation(context = applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,15 @@ package com.example.caloryapp.foodmodel
|
|||
|
||||
enum class FoodCategory(
|
||||
val displayName: String,
|
||||
val caloriesPer100g: Int,
|
||||
val caloriesPerPorsi: Int,
|
||||
val colorHex: String,
|
||||
val icon: String
|
||||
) {
|
||||
CARBS("Karbohidrat", 150, "#FECD45", "🍚"), // Tetap 150 kal/100g
|
||||
PROTEIN("Protein", 250, "#FC8369", "🍗"), // Ubah ke 250 kal/100g
|
||||
VEGETABLES("Sayuran", 30, "#4AB54A", "🥦"), // Tetap 30 kal/100g
|
||||
FRUITS("Buah", 60, "#FF6B6B", "🍎"), // Tetap 60 kal/100g
|
||||
OTHER("Lainnya", 200, "#A0A0A0", "🍬") // Tetap 200 kal/100g
|
||||
CARBS("Karbohidrat", 85, "#FECD45", "🍚"), // 1/3 piring
|
||||
PROTEIN("Protein", 25, "#FC8369", "🍗"), // 1/6 piring
|
||||
VEGETABLES("Sayuran", 25, "#4AB54A", "🥦"), // 1/3 piring
|
||||
FRUITS("Buah", 25, "#FF6B6B", "🍎"), // 1/6 piring
|
||||
// OTHER("Lainnya", 200, "#A0A0A0", "🍬") // Tetap 200 kal/100g
|
||||
}
|
||||
|
||||
data class FoodDetectionResult(
|
||||
|
@ -23,6 +23,6 @@ data class FoodDetectionResult(
|
|||
fun Map<FoodCategory, Float>.calculateTotalCalories(plateWeightGrams: Int = 500): Int {
|
||||
return this.entries.sumOf { (category, percentage) ->
|
||||
val weightGrams = plateWeightGrams * percentage
|
||||
(weightGrams * category.caloriesPer100g / 100).toInt()
|
||||
(weightGrams * category.caloriesPerPorsi / 100).toInt()
|
||||
}
|
||||
}
|
|
@ -15,8 +15,7 @@ class FoodDetector(private val context: Context) {
|
|||
FoodCategory.CARBS,
|
||||
FoodCategory.PROTEIN,
|
||||
FoodCategory.VEGETABLES,
|
||||
FoodCategory.FRUITS,
|
||||
FoodCategory.OTHER
|
||||
FoodCategory.FRUITS
|
||||
)
|
||||
|
||||
init {
|
||||
|
@ -24,7 +23,7 @@ class FoodDetector(private val context: Context) {
|
|||
}
|
||||
|
||||
private fun loadModel() {
|
||||
val assetFileDescriptor = context.assets.openFd("model_food_plate_densenet.tflite")
|
||||
val assetFileDescriptor = context.assets.openFd("model_food_plate_densenet5.tflite")
|
||||
val fileInputStream = FileInputStream(assetFileDescriptor.fileDescriptor)
|
||||
val fileChannel = fileInputStream.channel
|
||||
val startOffset = assetFileDescriptor.startOffset
|
||||
|
@ -63,7 +62,7 @@ class FoodDetector(private val context: Context) {
|
|||
modelInput.rewind() // Penting: reset posisi buffer ke awal
|
||||
|
||||
// Run model - pastikan outputBuffer memiliki dimensi yang benar
|
||||
val outputBuffer = Array(1) { FloatArray(5) } // 5 categories
|
||||
val outputBuffer = Array(1) { FloatArray(4) } // 5 categories
|
||||
interpreter?.run(modelInput, outputBuffer)
|
||||
|
||||
// Get predicted category
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package com.example.caloryapp.model
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
data class CaloryModel(
|
||||
val calories: Int = 0,
|
||||
val date: String = "",
|
||||
val username: String = "",
|
||||
val imagePath: String = "",
|
||||
)
|
||||
|
||||
fun CaloryModel.isToday(): Boolean {
|
||||
val today = Calendar.getInstance()
|
||||
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
return format.format(today.time) == this.date
|
||||
}
|
||||
|
||||
fun CaloryModel.isThisWeek(): Boolean {
|
||||
val cal = Calendar.getInstance()
|
||||
val currentWeek = cal.get(Calendar.WEEK_OF_YEAR)
|
||||
val currentYear = cal.get(Calendar.YEAR)
|
||||
|
||||
val modelDate = Calendar.getInstance().apply {
|
||||
time = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(this@isThisWeek.date)!!
|
||||
}
|
||||
|
||||
return modelDate.get(Calendar.WEEK_OF_YEAR) == currentWeek &&
|
||||
modelDate.get(Calendar.YEAR) == currentYear
|
||||
}
|
||||
|
||||
fun CaloryModel.isThisMonth(): Boolean {
|
||||
val cal = Calendar.getInstance()
|
||||
val currentMonth = cal.get(Calendar.MONTH)
|
||||
val currentYear = cal.get(Calendar.YEAR)
|
||||
|
||||
val modelDate = Calendar.getInstance().apply {
|
||||
time = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(this@isThisMonth.date)!!
|
||||
}
|
||||
|
||||
return modelDate.get(Calendar.MONTH) == currentMonth &&
|
||||
modelDate.get(Calendar.YEAR) == currentYear
|
||||
}
|
|
@ -1,19 +1,25 @@
|
|||
package com.example.caloryapp.navigation
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.example.caloryapp.viewmodel.FoodDetectionViewModel
|
||||
import com.example.caloryapp.pages.MainScreen
|
||||
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
||||
import com.example.caloryapp.pages.account.ProfileDetailScreen
|
||||
import com.example.caloryapp.pages.account.ProfileScreen
|
||||
import com.example.caloryapp.pages.calorydetail.CaloryDetailScreen
|
||||
import com.example.caloryapp.pages.calorydetail.ScreenTest
|
||||
import com.example.caloryapp.pages.onboard.ChangePasswordScreen
|
||||
import com.example.caloryapp.pages.onboard.ForgotPasswordScreen
|
||||
|
@ -23,11 +29,14 @@ import com.example.caloryapp.pages.onboard.OnBoardingScreen
|
|||
import com.example.caloryapp.pages.onboard.RegisterScreen
|
||||
import com.example.caloryapp.pages.onboard.SuccessChangePassword
|
||||
import com.example.caloryapp.pages.onboard.SuccessRegister
|
||||
import com.example.caloryapp.repository.UserRepository
|
||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||
import com.example.caloryapp.viewmodel.LoginState
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
@Composable
|
||||
fun Navigation(modifier: Modifier = Modifier) {
|
||||
fun Navigation(modifier: Modifier = Modifier, context: Context) {
|
||||
val navController = rememberNavController()
|
||||
val userViewModel: UserViewModel = viewModel()
|
||||
val foodViewModel: FoodDetectionViewModel = viewModel()
|
||||
|
@ -35,6 +44,44 @@ fun Navigation(modifier: Modifier = Modifier) {
|
|||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
userViewModel.initSessionManager(context)
|
||||
}
|
||||
|
||||
// Menentukan startDestination berdasarkan status login
|
||||
val startDestination = if (userViewModel.checkLoginStatus()) {
|
||||
NavigationScreen.MainScreen.name
|
||||
} else {
|
||||
NavigationScreen.LoginScreen.name
|
||||
}
|
||||
|
||||
// Observer untuk status login
|
||||
val loginState by userViewModel.loginState
|
||||
|
||||
// Navigasi otomatis ketika login berhasil
|
||||
LaunchedEffect(loginState) {
|
||||
when (loginState) {
|
||||
is LoginState.Success -> {
|
||||
navController.navigate(NavigationScreen.MainScreen.name) {
|
||||
popUpTo(NavigationScreen.LoginScreen.name) { inclusive = true }
|
||||
}
|
||||
}
|
||||
is LoginState.AlreadyLoggedIn -> {
|
||||
if (navController.currentDestination?.route != NavigationScreen.MainScreen.name) {
|
||||
navController.navigate(NavigationScreen.MainScreen.name) {
|
||||
popUpTo(NavigationScreen.LoginScreen.name) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
is LoginState.LoggedOut -> {
|
||||
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = NavigationScreen.LoginScreen.name
|
||||
|
@ -55,7 +102,7 @@ fun Navigation(modifier: Modifier = Modifier) {
|
|||
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
||||
}
|
||||
composable(NavigationScreen.ScreenTest.name) {
|
||||
ScreenTest(navController = navController, viewModel = foodViewModel)
|
||||
ScreenTest(navController = navController, viewModel = foodViewModel, userViewModel = userViewModel)
|
||||
}
|
||||
composable(NavigationScreen.ProfileScreen.name) {
|
||||
ProfileScreen(
|
||||
|
@ -65,6 +112,30 @@ fun Navigation(modifier: Modifier = Modifier) {
|
|||
drawerState = drawerState
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = "${NavigationScreen.CaloryDetailScreen.name}/{date}/{calories}/{imagePath}",
|
||||
arguments = listOf(
|
||||
navArgument("date") { type = NavType.StringType },
|
||||
navArgument("calories") { type = NavType.IntType },
|
||||
navArgument("imagePath") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val date = backStackEntry.arguments?.getString("date") ?: ""
|
||||
val calories = backStackEntry.arguments?.getInt("calories") ?: 0
|
||||
val imagePath = backStackEntry.arguments?.getString("imagePath") ?: ""
|
||||
|
||||
// Decode imagePath yang sudah diencoding
|
||||
val decodedImagePath = java.net.URLDecoder.decode(imagePath, StandardCharsets.UTF_8.name())
|
||||
|
||||
CaloryDetailScreen(
|
||||
navController = navController,
|
||||
caloryDate = date,
|
||||
caloryCalories = calories,
|
||||
caloryImagePath = decodedImagePath,
|
||||
userViewModel = userViewModel,
|
||||
caloryHistoryViewModel = caloryHistoryViewModel
|
||||
)
|
||||
}
|
||||
// composable(NavigationScreen.ScreenTest.name) {
|
||||
// ScreenTest(navController = navController, viewModel = userViewModel)
|
||||
// }
|
||||
|
|
|
@ -15,6 +15,7 @@ enum class NavigationScreen {
|
|||
ProfileScreen,
|
||||
ProfileDetailScreen,
|
||||
ProfileChangePasswordScreen,
|
||||
CaloryDetailScreen,
|
||||
ScreenTest;
|
||||
|
||||
fun fromRoute(route: String): NavigationScreen =
|
||||
|
@ -33,6 +34,7 @@ enum class NavigationScreen {
|
|||
SuccessRegister.name -> SuccessRegister
|
||||
ProfileDetailScreen.name -> ProfileDetailScreen
|
||||
ProfileChangePasswordScreen.name -> ProfileChangePasswordScreen
|
||||
CaloryDetailScreen.name -> CaloryDetailScreen
|
||||
ScreenTest.name -> ScreenTest
|
||||
|
||||
else -> throw IllegalArgumentException("$route gagal bji")
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
package com.example.caloryapp.pages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.DrawerValue
|
||||
|
@ -20,11 +25,14 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
@ -48,8 +56,15 @@ import com.example.caloryapp.ui.theme.medium
|
|||
import com.example.caloryapp.ui.theme.primary
|
||||
import com.example.caloryapp.ui.theme.semibold
|
||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||
import com.example.caloryapp.viewmodel.LoginState
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import android.util.Log
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.example.caloryapp.pages.calorydetail.CaloryDetailScreen
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
sealed class DrawerScreen(val title: String) {
|
||||
data object HomeScreen : DrawerScreen("Home")
|
||||
|
@ -66,11 +81,31 @@ fun MainScreen(
|
|||
val navController = rememberNavController()
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
// Observer untuk status login
|
||||
val loginState by userViewModel.loginState
|
||||
|
||||
// Navigasi otomatis ketika logout
|
||||
LaunchedEffect(loginState) {
|
||||
if (loginState is LoginState.LoggedOut) {
|
||||
// Navigasi ke login screen saat logout
|
||||
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
// Mencegah navigasi kembali ke login
|
||||
Toast.makeText(context, "Gunakan menu logout untuk keluar", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
DrawerContent(navController, drawerState, scope, userViewModel)
|
||||
// Menggunakan fungsi DrawerContent yang dimodifikasi dengan penanganan null
|
||||
SafeDrawerContent(navController, drawerState, scope, userViewModel)
|
||||
}
|
||||
) {
|
||||
NavHost(navController = navController, startDestination = DrawerScreen.HomeScreen.title) {
|
||||
|
@ -87,23 +122,123 @@ fun MainScreen(
|
|||
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
||||
}
|
||||
composable(NavigationScreen.ScreenTest.name) {
|
||||
ScreenTest(navController = navController, viewModel = foodDetectionViewModel)
|
||||
ScreenTest(navController = navController, viewModel = foodDetectionViewModel, userViewModel = userViewModel)
|
||||
}
|
||||
composable(NavigationScreen.LoginScreen.name) {
|
||||
LoginScreen(navController = navController, viewModel = userViewModel)
|
||||
}
|
||||
composable(
|
||||
route = "${NavigationScreen.CaloryDetailScreen.name}/{date}/{calories}/{imagePath}",
|
||||
arguments = listOf(
|
||||
navArgument("date") { type = NavType.StringType },
|
||||
navArgument("calories") { type = NavType.IntType },
|
||||
navArgument("imagePath") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val date = backStackEntry.arguments?.getString("date") ?: ""
|
||||
val calories = backStackEntry.arguments?.getInt("calories") ?: 0
|
||||
val imagePath = backStackEntry.arguments?.getString("imagePath") ?: ""
|
||||
|
||||
// Decode imagePath yang sudah diencoding
|
||||
val decodedImagePath = java.net.URLDecoder.decode(imagePath, StandardCharsets.UTF_8.name())
|
||||
|
||||
CaloryDetailScreen(
|
||||
navController = navController,
|
||||
caloryDate = date,
|
||||
caloryCalories = calories,
|
||||
caloryImagePath = decodedImagePath,
|
||||
userViewModel = userViewModel,
|
||||
caloryHistoryViewModel = caloryHistoryViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi Drawer Content yang aman dari NullPointerException
|
||||
@Composable
|
||||
fun DrawerContent(
|
||||
fun SafeDrawerContent(
|
||||
navController: NavHostController,
|
||||
drawerState: DrawerState,
|
||||
scope: kotlinx.coroutines.CoroutineScope,
|
||||
viewModel: UserViewModel
|
||||
) {
|
||||
val user = viewModel.user.value
|
||||
val context = LocalContext.current
|
||||
|
||||
// Jika user null, tampilkan drawer dengan konten minimal
|
||||
if (user == null) {
|
||||
ModalDrawerSheet(modifier = Modifier.background(primary)) {
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
// Header untuk user yang belum login
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 15.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_profile_women),
|
||||
contentDescription = null,
|
||||
Modifier.size(70.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "Belum Login",
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = Color.Black,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Divider(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
color = primary.copy(alpha = 0.1f),
|
||||
thickness = 3.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Menu navigasi standar
|
||||
DrawerItem(
|
||||
"Home",
|
||||
R.drawable.ic_home_filled,
|
||||
navController,
|
||||
DrawerScreen.HomeScreen.title,
|
||||
drawerState,
|
||||
scope
|
||||
)
|
||||
|
||||
// Tombol login
|
||||
TextButton(onClick = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_profile_filled),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(30.dp),
|
||||
tint = primary,
|
||||
)
|
||||
Text(
|
||||
"Login",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = primary,
|
||||
fontSize = 18.sp,
|
||||
fontFamily = bold,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Tampilkan drawer normal dengan informasi user
|
||||
ModalDrawerSheet(modifier = Modifier.background(primary)) {
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
Row(
|
||||
|
@ -119,7 +254,8 @@ fun DrawerContent(
|
|||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text(
|
||||
text = user!!.fullName,
|
||||
// Gunakan safe operator untuk menghindari null
|
||||
text = user.fullName,
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = Color.Black,
|
||||
|
@ -135,7 +271,6 @@ fun DrawerContent(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
@ -161,9 +296,47 @@ fun DrawerContent(
|
|||
drawerState,
|
||||
scope
|
||||
)
|
||||
|
||||
// Tambahkan separator dan tombol logout
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Divider(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
color = primary.copy(alpha = 0.1f),
|
||||
thickness = 1.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Tombol logout
|
||||
// LogoutDrawerItem(
|
||||
// title = "Logout",
|
||||
// icon = R.drawable.ic_profile_filled, // Gunakan icon yang sudah ada jika ic_logout tidak tersedia
|
||||
// onLogout = {
|
||||
// try {
|
||||
// // Navigasi ke login screen terlebih dahulu
|
||||
// scope.launch {
|
||||
// drawerState.close()
|
||||
//
|
||||
// // Navigasi ke login screen
|
||||
// navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||
// popUpTo(0) { inclusive = true }
|
||||
// }
|
||||
//
|
||||
// // Delay sedikit untuk memastikan navigasi sudah berjalan
|
||||
// delay(100)
|
||||
//
|
||||
// // Lakukan logout
|
||||
// viewModel.logout()
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// Log.e("MainScreen", "Error during logout: ${e.message}")
|
||||
// Toast.makeText(context, "Gagal logout: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk item menu drawer
|
||||
@Composable
|
||||
fun DrawerItem(
|
||||
title: String,
|
||||
|
@ -197,3 +370,28 @@ fun DrawerItem(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahkan fungsi baru untuk tombol logout
|
||||
@Composable
|
||||
fun LogoutDrawerItem(
|
||||
title: String,
|
||||
icon: Int,
|
||||
onLogout: () -> Unit
|
||||
) {
|
||||
TextButton(onClick = onLogout) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(30.dp),
|
||||
tint = Color.Red, // Warna merah untuk menandakan logout
|
||||
)
|
||||
Text(
|
||||
title,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = Color.Red,
|
||||
fontSize = 18.sp,
|
||||
fontFamily = bold,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
|
@ -30,6 +31,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
|
@ -161,7 +163,8 @@ fun ProfileDetailScreen(
|
|||
value = fullName,
|
||||
onValueChange = { fullName = it },
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
input = isEditing
|
||||
input = isEditing,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
|
@ -179,7 +182,8 @@ fun ProfileDetailScreen(
|
|||
value = username,
|
||||
onValueChange = { username = it },
|
||||
placeholderText = "Masukkan Username",
|
||||
input = false // Tidak dapat mengedit username
|
||||
input = false, // Tidak dapat mengedit username
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
|
@ -197,7 +201,8 @@ fun ProfileDetailScreen(
|
|||
value = gmail,
|
||||
onValueChange = { gmail = it },
|
||||
placeholderText = "Masukkan Email",
|
||||
input = isEditing
|
||||
input = isEditing,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
|
@ -215,7 +220,8 @@ fun ProfileDetailScreen(
|
|||
value = selectedGender,
|
||||
onValueChange = { selectedGender = it },
|
||||
placeholderText = "Masukkan Gender",
|
||||
input = isEditing
|
||||
input = isEditing,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
|
@ -233,7 +239,8 @@ fun ProfileDetailScreen(
|
|||
value = weight,
|
||||
onValueChange = { weight = it },
|
||||
placeholderText = "Masukkan Berat Badan",
|
||||
input = isEditing
|
||||
input = isEditing,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
|
@ -251,7 +258,8 @@ fun ProfileDetailScreen(
|
|||
value = height,
|
||||
onValueChange = { height = it },
|
||||
placeholderText = "Masukkan Tinggi Badan",
|
||||
input = isEditing
|
||||
input = isEditing,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
|
||||
// Tombol Edit
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.example.caloryapp.pages.account
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
@ -14,10 +16,13 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
@ -45,9 +50,11 @@ import com.example.caloryapp.ui.theme.primaryblack
|
|||
import com.example.caloryapp.ui.theme.primarygrey
|
||||
import com.example.caloryapp.ui.theme.primaryred
|
||||
import com.example.caloryapp.ui.theme.regular
|
||||
import com.example.caloryapp.viewmodel.LoginState
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import com.example.caloryapp.widget.SimpleAlertDialog
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
|
@ -70,6 +77,35 @@ fun ProfileScreen(
|
|||
val currentDate = Calendar.getInstance().time
|
||||
val dateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", Locale("id", "ID"))
|
||||
val formattedDate = dateFormat.format(currentDate)
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.initSessionManager(context)
|
||||
}
|
||||
|
||||
// Amati state login untuk navigasi
|
||||
val loginState by viewModel.loginState
|
||||
|
||||
// Navigasi otomatis saat logout
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.initSessionManager(context)
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
// Opsi 1: Tampilkan loading
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
// Opsi 2: Navigasi ke login screen
|
||||
LaunchedEffect(Unit) {
|
||||
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
return // Penting! Hentikan komposisi di sini
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier
|
||||
|
@ -228,7 +264,17 @@ fun ProfileScreen(
|
|||
dialogSubTitle = "Apakah Anda yakin ingin keluar?",
|
||||
onDismissRequest = { openAlertDialog.value = false },
|
||||
onConfirmation = {
|
||||
logoutUser()
|
||||
try {
|
||||
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
// delay(100)
|
||||
viewModel.logout()
|
||||
// Navigasi dilakukan oleh LaunchedEffect di atas
|
||||
} catch (e: Exception) {
|
||||
Log.e("ProfileScreen", "Error during logout: ${e.message}")
|
||||
Toast.makeText(context, "Gagal logout: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
openAlertDialog.value = false
|
||||
// Navigation()
|
||||
}
|
||||
|
|
|
@ -1,269 +1,247 @@
|
|||
package com.example.caloryapp.pages.calorydetail
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
//import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.caloryapp.foodmodel.FoodCategory
|
||||
import com.example.caloryapp.foodmodel.PlateDiagram
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.example.caloryapp.ui.theme.background
|
||||
import com.example.caloryapp.ui.theme.bold
|
||||
import com.example.caloryapp.ui.theme.medium
|
||||
import com.example.caloryapp.ui.theme.primary
|
||||
import com.example.caloryapp.ui.theme.primaryblack
|
||||
import com.example.caloryapp.ui.theme.semibold
|
||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Layar detail riwayat kalori
|
||||
*/
|
||||
@Composable
|
||||
fun CaloryHistoryDetailScreen(
|
||||
fun CaloryDetailScreen(
|
||||
navController: NavController,
|
||||
historyId: String,
|
||||
viewModel: CaloryHistoryViewModel
|
||||
caloryDate: String,
|
||||
caloryCalories: Int,
|
||||
caloryImagePath: String,
|
||||
userViewModel: UserViewModel,
|
||||
caloryHistoryViewModel: CaloryHistoryViewModel
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val user = userViewModel.user.value
|
||||
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
var isDeleting by remember { mutableStateOf(false) }
|
||||
|
||||
// Muat data detail saat komponen ditampilkan
|
||||
LaunchedEffect(historyId) {
|
||||
viewModel.loadHistoryDetail(historyId)
|
||||
}
|
||||
|
||||
// Bersihkan state detail saat komponen dihancurkan
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
viewModel.selectedHistory = null
|
||||
// Muat gambar jika ada path
|
||||
LaunchedEffect(caloryImagePath) {
|
||||
if (caloryImagePath.isNotEmpty()) {
|
||||
bitmap = caloryHistoryViewModel.getImageBitmap(context, caloryImagePath)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(background)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 25.dp, vertical = 45.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Header dengan tombol kembali
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { navController.popBackStack() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Kembali",
|
||||
tint = primary
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Detail Kalori",
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(
|
||||
text = "Profil Saya",
|
||||
style = TextStyle(
|
||||
fontSize = 24.sp,
|
||||
color = primary,
|
||||
fontSize = 25.sp,
|
||||
color = primaryblack,
|
||||
fontFamily = bold
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Tombol hapus
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
viewModel.deleteHistory(historyId) { success ->
|
||||
)
|
||||
) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.navigateUp() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowLeft,
|
||||
contentDescription = null,
|
||||
Modifier
|
||||
.size(28.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// Tombol hapus
|
||||
IconButton(onClick = {
|
||||
if (user != null) {
|
||||
isDeleting = true
|
||||
caloryHistoryViewModel.deleteCaloryData(
|
||||
date = caloryDate,
|
||||
calories = caloryCalories,
|
||||
imagePath = caloryImagePath,
|
||||
username = user.username
|
||||
) { success ->
|
||||
isDeleting = false
|
||||
if (success) {
|
||||
navController.popBackStack()
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
},
|
||||
backgroundColor = background
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.background(background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isDeleting) {
|
||||
CircularProgressIndicator(color = primary)
|
||||
} else {
|
||||
// Detail kalori
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Hapus",
|
||||
tint = Color.Red
|
||||
// Gambar makanan
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(250.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.LightGray),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
bitmap?.let { bmp ->
|
||||
Image(
|
||||
bitmap = bmp.asImageBitmap(),
|
||||
contentDescription = "Food Image",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} ?: Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = "No Image",
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Informasi kalori
|
||||
Text(
|
||||
text = "$caloryCalories Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 32.sp,
|
||||
color = primary,
|
||||
fontFamily = bold
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Tampilkan loading atau konten
|
||||
if (viewModel.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
color = primary
|
||||
)
|
||||
} else if (viewModel.selectedHistory != null) {
|
||||
val history = viewModel.selectedHistory!!
|
||||
|
||||
// Total kalori
|
||||
Text(
|
||||
text = "${history.totalCalories}",
|
||||
style = TextStyle(
|
||||
fontSize = 48.sp,
|
||||
color = primary,
|
||||
fontFamily = bold
|
||||
),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = "Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 24.sp,
|
||||
color = primary,
|
||||
fontFamily = semibold
|
||||
),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Tanggal dan waktu
|
||||
val dateFormat = SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
|
||||
Text(
|
||||
text = "Tercatat pada ${dateFormat.format(history.getDate())}",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = Color.Gray,
|
||||
fontWeight = FontWeight.Normal
|
||||
),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Diagram komposisi makanan
|
||||
// Konversi kembali Map<String, Float> ke Map<FoodCategory, Float>
|
||||
val foodCategories = history.foodComposition.mapKeys { entry ->
|
||||
FoodCategory.values().find { it.displayName == entry.key } ?: FoodCategory.OTHER
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
PlateDiagram(
|
||||
categories = foodCategories,
|
||||
modifier = Modifier.size(200.dp)
|
||||
Text(
|
||||
text = "Tanggal: ${formatDate(caloryDate)}",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.Gray,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Breakdown kalori per kategori
|
||||
Text(
|
||||
text = "Komposisi Kalori:",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Tampilkan breakdown per kategori
|
||||
history.foodComposition.forEach { (categoryName, percentage) ->
|
||||
val category = FoodCategory.values().find { it.displayName == categoryName }
|
||||
val colorHex = category?.colorHex ?: "#A0A0A0"
|
||||
val icon = category?.icon ?: "🍽️"
|
||||
val calories = history.caloriesPerCategory[categoryName] ?: 0
|
||||
|
||||
Row(
|
||||
// Tombol hapus
|
||||
Button(
|
||||
onClick = {
|
||||
if (user != null) {
|
||||
isDeleting = true
|
||||
caloryHistoryViewModel.deleteCaloryData(
|
||||
date = caloryDate,
|
||||
calories = caloryCalories,
|
||||
imagePath = caloryImagePath,
|
||||
username = user.username
|
||||
) { success ->
|
||||
isDeleting = false
|
||||
if (success) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
.height(56.dp),
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
enabled = !isDeleting
|
||||
) {
|
||||
Text(
|
||||
text = "$icon $categoryName",
|
||||
color = Color(android.graphics.Color.parseColor(colorHex))
|
||||
text = "Hapus Data Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = "$calories kal",
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = "(%.0f%%)".format(percentage * 100),
|
||||
color = Color.Gray,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Tombol kembali ke riwayat
|
||||
Button(
|
||||
onClick = { navController.popBackStack() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = primary)
|
||||
) {
|
||||
Text(
|
||||
text = "Kembali ke Riwayat",
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
} else if (viewModel.errorMessage != null) {
|
||||
// Error message
|
||||
Text(
|
||||
text = viewModel.errorMessage!!,
|
||||
color = Color.Red,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
} else {
|
||||
// Jika data tidak ditemukan
|
||||
Text(
|
||||
text = "Data tidak ditemukan",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk memformat tanggal
|
||||
fun formatDate(dateString: String): String {
|
||||
return try {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault())
|
||||
val date = inputFormat.parse(dateString)
|
||||
outputFormat.format(date ?: Date())
|
||||
} catch (e: Exception) {
|
||||
dateString // Kembalikan string asli jika format tidak sesuai
|
||||
}
|
||||
}
|
|
@ -320,7 +320,293 @@ package com.example.caloryapp.pages.calorydetail
|
|||
|
||||
//package com.example.caloryapp.pages.camera
|
||||
|
||||
//import android.net.Uri
|
||||
//import android.widget.Toast
|
||||
//import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
//import androidx.activity.result.contract.ActivityResultContracts
|
||||
//import androidx.compose.foundation.Image
|
||||
//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.Spacer
|
||||
//import androidx.compose.foundation.layout.fillMaxSize
|
||||
//import androidx.compose.foundation.layout.fillMaxWidth
|
||||
//import androidx.compose.foundation.layout.height
|
||||
//import androidx.compose.foundation.layout.padding
|
||||
//import androidx.compose.foundation.layout.size
|
||||
//import androidx.compose.foundation.layout.width
|
||||
//import androidx.compose.foundation.rememberScrollState
|
||||
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
//import androidx.compose.foundation.verticalScroll
|
||||
//import androidx.compose.material3.Button
|
||||
//import androidx.compose.material3.CircularProgressIndicator
|
||||
//import androidx.compose.material3.Text
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.runtime.getValue
|
||||
//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
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.layout.ContentScale
|
||||
//import androidx.compose.ui.platform.LocalContext
|
||||
//import androidx.compose.ui.text.TextStyle
|
||||
//import androidx.compose.ui.text.style.TextAlign
|
||||
//import androidx.compose.ui.text.style.TextDecoration
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//import androidx.navigation.NavController
|
||||
//import coil3.compose.rememberAsyncImagePainter
|
||||
//import com.example.caloryapp.viewmodel.FoodDetectionViewModel
|
||||
//import com.example.caloryapp.foodmodel.PlateDiagram
|
||||
//import com.example.caloryapp.foodmodel.calculateTotalCalories
|
||||
//import com.example.caloryapp.repository.CaloryRepository
|
||||
//import com.example.caloryapp.repository.UserRepository
|
||||
//import com.example.caloryapp.ui.theme.bold
|
||||
//import com.example.caloryapp.ui.theme.medium
|
||||
//import com.example.caloryapp.ui.theme.primary
|
||||
//import com.example.caloryapp.ui.theme.primaryblack
|
||||
//import com.example.caloryapp.ui.theme.semibold
|
||||
//import com.example.caloryapp.viewmodel.UserViewModel
|
||||
//import com.google.firebase.auth.FirebaseAuth
|
||||
//
|
||||
//@Composable
|
||||
//fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel, userViewModel: UserViewModel) {
|
||||
// val context = LocalContext.current
|
||||
// var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
// var saveMessage by remember { mutableStateOf<String?>(null) }
|
||||
// var isScanned by remember { mutableStateOf(false) }
|
||||
// val caloryRepository = CaloryRepository()
|
||||
// val viewmodel = userViewModel.user.value
|
||||
//
|
||||
//
|
||||
// val getContent = rememberLauncherForActivityResult(
|
||||
// contract = ActivityResultContracts.GetContent()
|
||||
// ) { uri: Uri? ->
|
||||
// uri?.let {
|
||||
// selectedImageUri = it
|
||||
// viewModel.detectFoodFromImage(context, it)
|
||||
// isScanned = true
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Column(
|
||||
// modifier = Modifier
|
||||
// .fillMaxSize()
|
||||
// .padding(horizontal = 25.dp, vertical = 50.dp)
|
||||
// .verticalScroll(rememberScrollState()),
|
||||
// ) {
|
||||
// Spacer(modifier = Modifier.height(60.dp))
|
||||
//
|
||||
// Text(
|
||||
// text = "Pindai Makanan Kamu",
|
||||
// style = TextStyle(
|
||||
// fontSize = 38.sp,
|
||||
// color = primary,
|
||||
// fontFamily = bold
|
||||
// )
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(18.dp))
|
||||
//
|
||||
// Text(
|
||||
// text = "Penuhi Kebutuhan Kalori Kamu Hari ini Yuk!",
|
||||
// style = TextStyle(
|
||||
// fontSize = 21.sp,
|
||||
// color = primary,
|
||||
// fontFamily = bold
|
||||
// )
|
||||
// )
|
||||
//
|
||||
// // Selected image preview
|
||||
// if (selectedImageUri != null) {
|
||||
// Spacer(modifier = Modifier.height(40.dp))
|
||||
// Row(Modifier.fillMaxWidth(), Arrangement.Center) {
|
||||
// Image(
|
||||
// painter = rememberAsyncImagePainter(selectedImageUri),
|
||||
// contentDescription = "Selected image",
|
||||
// modifier = Modifier
|
||||
// .size(250.dp)
|
||||
// .clip(RoundedCornerShape(8.dp)),
|
||||
// contentScale = ContentScale.Crop
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
// }
|
||||
// Spacer(modifier = Modifier.height(40.dp))
|
||||
// } else {
|
||||
// // Placeholder
|
||||
// Spacer(modifier = Modifier.height(40.dp))
|
||||
// Row(Modifier.fillMaxWidth(), Arrangement.Center) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .size(250.dp)
|
||||
// .clip(RoundedCornerShape(8.dp))
|
||||
// .padding(16.dp),
|
||||
// contentAlignment = Alignment.Center
|
||||
// ) {
|
||||
// androidx.compose.material.Text(
|
||||
// text = "Pilih Gambar Makanan",
|
||||
// style = TextStyle(
|
||||
// fontSize = 18.sp,
|
||||
// color = primary,
|
||||
// letterSpacing = 1.sp,
|
||||
// fontFamily = medium,
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
// }
|
||||
// Spacer(modifier = Modifier.height(40.dp))
|
||||
// }
|
||||
//
|
||||
// // Button to select image
|
||||
// androidx.compose.material.Button(
|
||||
// onClick = {
|
||||
// if (isScanned) {
|
||||
// val username = viewmodel!!.username
|
||||
// val totalCalories = viewModel.detectionResult?.allCategories?.calculateTotalCalories() ?: 0
|
||||
// caloryRepository.saveCalorieData(username, totalCalories) { success ->
|
||||
// if (success) {
|
||||
// Toast.makeText(
|
||||
// context,
|
||||
// "Berhasil",
|
||||
// Toast.LENGTH_SHORT
|
||||
// ).show()
|
||||
// isScanned = false
|
||||
// } else {
|
||||
// Toast.makeText(
|
||||
// context,
|
||||
// "Gagal",
|
||||
// Toast.LENGTH_SHORT
|
||||
// ).show()
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// getContent.launch("image/*")
|
||||
// }
|
||||
// },
|
||||
// modifier = Modifier
|
||||
// .width(360.dp)
|
||||
// .height(50.dp),
|
||||
// colors = androidx.compose.material.ButtonDefaults.buttonColors(backgroundColor = primary),
|
||||
// shape = RoundedCornerShape(20.dp)
|
||||
// ) {
|
||||
// androidx.compose.material.Text(
|
||||
// text = if (isScanned) "Simpan" else "Pilih",
|
||||
// style = TextStyle(
|
||||
// fontSize = 18.sp,
|
||||
// color = Color.White,
|
||||
// fontFamily = semibold,
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//// Button(
|
||||
//// onClick = { },
|
||||
//// modifier = Modifier.fillMaxWidth()
|
||||
//// ) {
|
||||
//// Text("Pilih Gambar dari Galeri")
|
||||
//// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(32.dp))
|
||||
//
|
||||
// if (viewModel.isLoading) {
|
||||
// CircularProgressIndicator()
|
||||
// }
|
||||
//
|
||||
// viewModel.errorMessage?.let { error ->
|
||||
// Text(
|
||||
// text = error,
|
||||
// color = Color.Red,
|
||||
// modifier = Modifier.padding(vertical = 8.dp)
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // Detection results
|
||||
// viewModel.detectionResult?.let { result ->
|
||||
// Column(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// horizontalAlignment = Alignment.CenterHorizontally
|
||||
// ) {
|
||||
//// Text(
|
||||
//// text = "Hasil Deteksi",
|
||||
//// fontSize = 20.sp,
|
||||
//// fontWeight = FontWeight.Bold,
|
||||
//// modifier = Modifier.padding(bottom = 16.dp)
|
||||
//// )
|
||||
//
|
||||
// // Food plate diagram
|
||||
//// PlateDiagram(
|
||||
//// categories = result.allCategories,
|
||||
//// modifier = Modifier
|
||||
//// .size(200.dp)
|
||||
//// .padding(8.dp)
|
||||
//// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
//
|
||||
// val totalCalories = result.allCategories.calculateTotalCalories()
|
||||
//
|
||||
//// Text(
|
||||
//// text = "Kategori Utama: ${result.mainCategory.displayName}",
|
||||
//// fontWeight = FontWeight.SemiBold
|
||||
//// )
|
||||
//
|
||||
// Text(
|
||||
// text = "$totalCalories",
|
||||
// style = TextStyle(
|
||||
// fontSize = 24.sp,
|
||||
// color = primary,
|
||||
// fontFamily = bold,
|
||||
// textDecoration = TextDecoration.Underline
|
||||
// )
|
||||
// )
|
||||
// Text(
|
||||
// text = "Kalori di Makanan Kamu Hari ini",
|
||||
// style = TextStyle(
|
||||
// fontSize = 18.sp,
|
||||
// color = primary,
|
||||
// fontFamily = semibold
|
||||
// )
|
||||
// )
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(20.dp))
|
||||
// result.allCategories.forEach { (category, confidence) ->
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = 4.dp),
|
||||
// horizontalArrangement = Arrangement.SpaceBetween
|
||||
// ) {
|
||||
// Text(
|
||||
// text = "${category.icon} ${category.displayName}",
|
||||
// color = Color(android.graphics.Color.parseColor(category.colorHex))
|
||||
// )
|
||||
// Text(
|
||||
// text = "%.0f%%".format(confidence * 100),
|
||||
// style = TextStyle(
|
||||
// fontSize = 14.sp,
|
||||
// color = primaryblack,
|
||||
// fontFamily = medium
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
|
@ -338,10 +624,12 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
@ -358,28 +646,80 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import com.example.caloryapp.viewmodel.FoodDetectionViewModel
|
||||
import com.example.caloryapp.foodmodel.PlateDiagram
|
||||
import com.example.caloryapp.foodmodel.calculateTotalCalories
|
||||
import com.example.caloryapp.repository.CaloryRepository
|
||||
import com.example.caloryapp.ui.theme.bold
|
||||
import com.example.caloryapp.ui.theme.medium
|
||||
import com.example.caloryapp.ui.theme.primary
|
||||
import com.example.caloryapp.ui.theme.primaryblack
|
||||
import com.example.caloryapp.ui.theme.semibold
|
||||
import com.example.caloryapp.utils.LocalImageStorage
|
||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel) {
|
||||
fun ScreenTest(
|
||||
navController: NavController,
|
||||
viewModel: FoodDetectionViewModel,
|
||||
userViewModel: UserViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var capturedBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
var isScanned by remember { mutableStateOf(false) }
|
||||
var isSaving by remember { mutableStateOf(false) }
|
||||
|
||||
// Tambahkan instance CaloryHistoryViewModel untuk menyimpan data dengan gambar
|
||||
val caloryHistoryViewModel = remember { CaloryHistoryViewModel() }
|
||||
val user = userViewModel.user.value
|
||||
|
||||
LaunchedEffect(viewModel.errorMessage) {
|
||||
viewModel.errorMessage?.let { error ->
|
||||
Toast.makeText(context, error, Toast.LENGTH_LONG).show()
|
||||
|
||||
// Reset state jika ada error tentang gambar bukan makanan
|
||||
if (error.contains("Pindai Makanan Gagal!")) {
|
||||
isScanned = false
|
||||
// Anda juga bisa menambahkan animasi shake atau highlight pada area pemilihan gambar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel.detectionResult) {
|
||||
// Jika deteksi berhasil dan hasilnya tidak null, maka gambar adalah makanan
|
||||
isScanned = viewModel.detectionResult != null
|
||||
}
|
||||
|
||||
// Launcher untuk memilih gambar
|
||||
val getContent = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let {
|
||||
selectedImageUri = it
|
||||
viewModel.detectFoodFromImage(context, it)
|
||||
|
||||
// Konversi URI ke Bitmap
|
||||
try {
|
||||
capturedBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val source = ImageDecoder.createSource(context.contentResolver, it)
|
||||
ImageDecoder.decodeBitmap(source)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
MediaStore.Images.Media.getBitmap(context.contentResolver, it)
|
||||
}
|
||||
|
||||
// Deteksi makanan dari gambar
|
||||
viewModel.detectFoodFromImage(context, it)
|
||||
isScanned = true
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "Gagal memuat gambar: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,7 +776,7 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
|||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
androidx.compose.material.Text(
|
||||
Text(
|
||||
text = "Pilih Gambar Makanan",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
|
@ -452,42 +792,74 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
|||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
|
||||
// Button to select image
|
||||
androidx.compose.material.Button(
|
||||
// Button to select or save image
|
||||
Button(
|
||||
onClick = {
|
||||
getContent.launch("image/*")
|
||||
if (isScanned && user != null && capturedBitmap != null) {
|
||||
// Jika sudah di-scan, simpan data dengan gambar
|
||||
isSaving = true
|
||||
|
||||
// Gunakan CaloryHistoryViewModel untuk menyimpan dengan gambar
|
||||
caloryHistoryViewModel.saveCaloryData(
|
||||
context = context,
|
||||
bitmap = capturedBitmap!!,
|
||||
calories = viewModel.detectionResult?.allCategories?.calculateTotalCalories() ?: 0,
|
||||
username = user.username
|
||||
) { success, message ->
|
||||
isSaving = false
|
||||
if (success) {
|
||||
Toast.makeText(context, "Berhasil menyimpan data kalori", Toast.LENGTH_SHORT).show()
|
||||
// Reset state setelah berhasil
|
||||
isScanned = false
|
||||
selectedImageUri = null
|
||||
capturedBitmap = null
|
||||
// Kembali ke halaman sebelumnya
|
||||
navController.navigateUp()
|
||||
} else {
|
||||
Toast.makeText(context, "Gagal menyimpan data: $message", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Jika belum scan, buka gallery
|
||||
getContent.launch("image/*")
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.width(360.dp)
|
||||
.height(50.dp),
|
||||
colors = androidx.compose.material.ButtonDefaults.buttonColors(backgroundColor = primary),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = primary),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
enabled = !isSaving // Disable saat proses penyimpanan
|
||||
) {
|
||||
androidx.compose.material.Text(
|
||||
text = "Pilih",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
if (isSaving) {
|
||||
CircularProgressIndicator(
|
||||
color = Color.White,
|
||||
fontFamily = semibold,
|
||||
textAlign = TextAlign.Center
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = if (isScanned) "Simpan" else "Pilih",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.White,
|
||||
fontFamily = semibold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Button(
|
||||
// onClick = { },
|
||||
// modifier = Modifier.fillMaxWidth()
|
||||
// ) {
|
||||
// Text("Pilih Gambar dari Galeri")
|
||||
// }
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Loading indicator
|
||||
if (viewModel.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = primary)
|
||||
}
|
||||
}
|
||||
|
||||
// Error message
|
||||
viewModel.errorMessage?.let { error ->
|
||||
Text(
|
||||
text = error,
|
||||
|
@ -502,31 +874,10 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Text(
|
||||
// text = "Hasil Deteksi",
|
||||
// fontSize = 20.sp,
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// modifier = Modifier.padding(bottom = 16.dp)
|
||||
// )
|
||||
|
||||
// Food plate diagram
|
||||
PlateDiagram(
|
||||
categories = result.allCategories,
|
||||
modifier = Modifier
|
||||
.size(200.dp)
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Main category and caloriesd
|
||||
val totalCalories = result.allCategories.calculateTotalCalories()
|
||||
|
||||
// Text(
|
||||
// text = "Kategori Utama: ${result.mainCategory.displayName}",
|
||||
// fontWeight = FontWeight.SemiBold
|
||||
// )
|
||||
|
||||
Text(
|
||||
text = "$totalCalories",
|
||||
style = TextStyle(
|
||||
|
@ -546,28 +897,123 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
|||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
result.allCategories.forEach { (category, confidence) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "${category.icon} ${category.displayName}",
|
||||
color = Color(android.graphics.Color.parseColor(category.colorHex))
|
||||
)
|
||||
Text(
|
||||
text = "%.0f%%".format(confidence * 100),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = primaryblack,
|
||||
fontFamily = medium
|
||||
// result.allCategories.forEach { (category, confidence) ->
|
||||
Column() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Karbohidrat",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = bold
|
||||
)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "50 %",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Protein",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = bold
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "30 %",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Sayur",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = bold
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "10 %",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Buah",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = bold
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "10 %",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
color = primary,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = 4.dp),
|
||||
// horizontalArrangement = Arrangement.SpaceBetween
|
||||
// ) {
|
||||
// Text(
|
||||
// text = "${category.icon} ${category.displayName}",
|
||||
// color = Color(android.graphics.Color.parseColor(category.colorHex))
|
||||
// )
|
||||
// Text(
|
||||
// text = "%.0f%%".format(confidence * 100),
|
||||
// style = TextStyle(
|
||||
// fontSize = 14.sp,
|
||||
// color = primaryblack,
|
||||
// fontFamily = medium
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.caloryapp.pages.dashboard
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
@ -21,6 +22,8 @@ import androidx.compose.material.Card
|
|||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -31,7 +34,9 @@ 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
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
|
@ -39,17 +44,31 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.caloryapp.R
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.example.caloryapp.model.isThisMonth
|
||||
import com.example.caloryapp.model.isThisWeek
|
||||
import com.example.caloryapp.model.isToday
|
||||
import com.example.caloryapp.navigation.NavigationScreen
|
||||
import com.example.caloryapp.repository.CaloryRepository
|
||||
import com.example.caloryapp.ui.theme.background
|
||||
import com.example.caloryapp.ui.theme.bold
|
||||
import com.example.caloryapp.ui.theme.medium
|
||||
import com.example.caloryapp.ui.theme.primary
|
||||
import com.example.caloryapp.ui.theme.primary2
|
||||
import com.example.caloryapp.ui.theme.primarygrey
|
||||
import com.example.caloryapp.ui.theme.semibold
|
||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||
import com.example.caloryapp.viewmodel.LoginState
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import com.example.caloryapp.widget.FilterBar
|
||||
import kotlinx.coroutines.launch
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
navController: NavController,
|
||||
|
@ -58,13 +77,55 @@ fun HomeScreen(
|
|||
caloryHistoryViewModel: CaloryHistoryViewModel,
|
||||
viewModel: UserViewModel,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var selectedFilter by remember { mutableStateOf("Semua") }
|
||||
val user = viewModel.user.value
|
||||
val caloryRepository = CaloryRepository()
|
||||
var calorieList by remember { mutableStateOf(listOf<CaloryModel>()) }
|
||||
|
||||
// Cek apakah user null, jika ya, arahkan ke login
|
||||
if (user == null) {
|
||||
LaunchedEffect(Unit) {
|
||||
Log.d("HomeScreen", "User adalah null, mengarahkan ke halaman login")
|
||||
Toast.makeText(context, "Silakan login terlebih dahulu", Toast.LENGTH_SHORT).show()
|
||||
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
// Tampilkan loading sampai navigasi selesai
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = primary)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Jika user tidak null, lanjutkan dengan logika normal
|
||||
val filteredList = when (selectedFilter) {
|
||||
"Hari ini" -> calorieList.filter { it.isToday() }
|
||||
"Minggu Ini" -> calorieList.filter { it.isThisWeek() }
|
||||
"Bulan Ini" -> calorieList.filter { it.isThisMonth() }
|
||||
else -> calorieList
|
||||
}
|
||||
|
||||
LaunchedEffect(user) {
|
||||
user?.let {
|
||||
try {
|
||||
// Memuat 4 riwayat terbaru untuk ditampilkan di home
|
||||
caloryHistoryViewModel.loadHistoryByUsername(it.username, 2)
|
||||
caloryHistoryViewModel.loadHistoryByUsername(user.username, 2)
|
||||
|
||||
// Gunakan try-catch untuk menangkap error saat mengambil data
|
||||
caloryRepository.getCalorieData(user.username) { data ->
|
||||
calorieList = data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("HomeScreen", "Error loading data: ${e.message}")
|
||||
// Tangani error dengan menampilkan pesan
|
||||
Toast.makeText(context, "Gagal memuat data: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,7 +151,7 @@ fun HomeScreen(
|
|||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = user!!.fullName,
|
||||
text = user.fullName, // Aman karena user sudah dipastikan tidak null di atas
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = Color.Black,
|
||||
|
@ -117,10 +178,10 @@ fun HomeScreen(
|
|||
|
||||
Spacer(modifier = Modifier.height(35.dp))
|
||||
|
||||
// Teks sapaan
|
||||
// Teks sapaan - Sudah aman karena user dipastikan tidak null
|
||||
Row(Modifier.width(215.dp)) {
|
||||
Text(
|
||||
text = "Hai ${user!!.fullName}, Bagaimana kabar kamu hari ini?",
|
||||
text = "Hai ${user.fullName}, Bagaimana kabar kamu hari ini?",
|
||||
style = TextStyle(
|
||||
fontSize = 22.sp,
|
||||
color = Color.Black,
|
||||
|
@ -129,13 +190,6 @@ fun HomeScreen(
|
|||
)
|
||||
}
|
||||
|
||||
// Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
// Tombol untuk membuka Navigation Drawer
|
||||
// Button(onClick = { }) {
|
||||
// Text(text = "Buka Menu")
|
||||
// }
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
androidx.compose.material3.Divider(color = primary.copy(alpha = 0.2f), thickness = 3.dp)
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
@ -144,12 +198,7 @@ fun HomeScreen(
|
|||
FilterBar(selectedFilter = selectedFilter, onFilterSelected = { selectedFilter = it })
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
if (caloryHistoryViewModel.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
color = primary
|
||||
)
|
||||
} else if (caloryHistoryViewModel.historyList.isEmpty()) {
|
||||
if (filteredList.isEmpty()) {
|
||||
Text(
|
||||
text = "Belum ada riwayat makanan",
|
||||
style = TextStyle(
|
||||
|
@ -162,61 +211,54 @@ fun HomeScreen(
|
|||
.padding(vertical = 24.dp)
|
||||
)
|
||||
} else {
|
||||
// List riwayat kalori menggunakan LazyColumn khusus
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
items(caloryHistoryViewModel.historyList) { history ->
|
||||
// Card item untuk riwayat kalori
|
||||
items(filteredList) { calory ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
// navController.navigate("${NavigationScreen.DetailHistory.name}/${history.id}")
|
||||
},
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF4AB54A).copy(alpha = 0.6f)) // Warna hijau sesuai gambar
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "${history.totalCalories} Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = Color.White,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
.size(width = 365.dp, height = 100.dp)
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxSize(),
|
||||
backgroundColor = primary2,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
onClick = {
|
||||
val encodedImagePath = URLEncoder.encode(calory.imagePath, StandardCharsets.UTF_8.name())
|
||||
|
||||
Text(
|
||||
text = "Lihat Detail",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = Color.White,
|
||||
fontFamily = medium,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Navigasi ke halaman detail
|
||||
navController.navigate(
|
||||
"${NavigationScreen.CaloryDetailScreen.name}/${calory.date}/${calory.calories}/${encodedImagePath}"
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
Arrangement.Center,) {
|
||||
Text(
|
||||
text = "${calory.calories} Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 26.sp,
|
||||
color = Color.White,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
androidx.compose.material3.Divider(modifier = Modifier.width(180.dp), color = Color.White, thickness = 3.dp)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Lihat Detail",
|
||||
style = TextStyle(
|
||||
fontSize = 13.sp,
|
||||
color = Color.White,
|
||||
fontFamily = semibold,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer di akhir list
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(70.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -32,6 +33,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
@ -100,7 +102,8 @@ fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
|||
value = username,
|
||||
onValueChange = { username = it },
|
||||
input = true,
|
||||
placeholderText = "Masukkan Username Anda"
|
||||
placeholderText = "Masukkan Username Anda",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
Spacer(modifier.height(20.dp))
|
||||
androidx.compose.material.Text(
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -23,6 +24,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
@ -80,7 +82,8 @@ fun ForgotPasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
|||
CustomTextField(
|
||||
value = gmail,
|
||||
onValueChange = { gmail = it },input = true,
|
||||
placeholderText = stringResource(R.string.gmail)
|
||||
placeholderText = stringResource(R.string.gmail),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||
)
|
||||
Spacer(modifier.height(40.dp))
|
||||
Button(
|
||||
|
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.MaterialTheme
|
||||
|
@ -30,10 +31,12 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -61,7 +64,7 @@ fun LoginScreen(
|
|||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
val state = viewModel.loginstate.value
|
||||
val state = viewModel.loginState.value
|
||||
val userData = viewModel.user.value
|
||||
val context = LocalContext.current
|
||||
|
||||
|
@ -115,7 +118,8 @@ fun LoginScreen(
|
|||
CustomTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it }, input = true,
|
||||
placeholderText = "Masukkan Username"
|
||||
placeholderText = "Masukkan Username",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
Spacer(modifier.height(16.dp))
|
||||
Text(
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Text
|
||||
|
@ -26,6 +27,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
@ -124,7 +126,8 @@ fun RegisterScreen(
|
|||
value = username,
|
||||
onValueChange = { username = it },
|
||||
placeholderText = "Masukkan Username",
|
||||
input = true
|
||||
input = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
// gmail
|
||||
|
@ -142,7 +145,8 @@ fun RegisterScreen(
|
|||
value = gmail,
|
||||
onValueChange = { gmail = it },
|
||||
input = true,
|
||||
placeholderText = "Masukkan Email"
|
||||
placeholderText = "Masukkan Email",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||
)
|
||||
|
||||
// nama lengkap
|
||||
|
@ -159,7 +163,8 @@ fun RegisterScreen(
|
|||
CustomTextField(
|
||||
value = fullName,
|
||||
onValueChange = { fullName = it }, input = true,
|
||||
placeholderText = "Masukkan Nama Lengkap"
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
// pw
|
||||
|
@ -179,7 +184,9 @@ fun RegisterScreen(
|
|||
CustomTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it }, input = true,
|
||||
placeholderText = "Masukkan Kata Sandi"
|
||||
placeholderText = "Masukkan Kata Sandi",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
|
||||
)
|
||||
|
||||
// gender
|
||||
|
@ -211,7 +218,9 @@ fun RegisterScreen(
|
|||
CustomTextField(
|
||||
value = weight.toString(),
|
||||
onValueChange = { weight = it }, input = true,
|
||||
placeholderText = "Masukkan Berat Badan"
|
||||
placeholderText = "Masukkan Berat Badan",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
|
||||
)
|
||||
|
||||
// tinggi badan
|
||||
|
@ -228,7 +237,8 @@ fun RegisterScreen(
|
|||
CustomTextField(
|
||||
value = height.toString(),
|
||||
onValueChange = { height = it }, input = true,
|
||||
placeholderText = "Masukkan Tinggi Badan"
|
||||
placeholderText = "Masukkan Tinggi Badan",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
|
||||
// btn daftar
|
||||
|
|
|
@ -1,17 +1,246 @@
|
|||
package com.example.caloryapp.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import com.example.caloryapp.foodmodel.FoodCategory
|
||||
import com.example.caloryapp.foodmodel.FoodDetectionResult
|
||||
import com.example.caloryapp.model.CaloryHistoryModel
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.example.caloryapp.utils.LocalImageStorage
|
||||
import com.google.firebase.Timestamp
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import com.google.firebase.firestore.Query
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
class CaloryRepository {
|
||||
private val db = FirebaseFirestore.getInstance()
|
||||
private val TAG = "CaloryRepository"
|
||||
private val db = FirebaseFirestore.getInstance()
|
||||
|
||||
// Mendapatkan data kalori dari database
|
||||
fun getCalorieData(username: String, onComplete: (List<CaloryModel>) -> Unit) {
|
||||
db.collection("users")
|
||||
.whereEqualTo("username", username)
|
||||
.get()
|
||||
.addOnSuccessListener { result ->
|
||||
if (!result.isEmpty) {
|
||||
val userDocument = result.documents[0]
|
||||
userDocument.reference.collection("calorieData")
|
||||
.orderBy("date", Query.Direction.DESCENDING)
|
||||
.get()
|
||||
.addOnSuccessListener { snapshot ->
|
||||
val calorieList = mutableListOf<CaloryModel>()
|
||||
for (document in snapshot.documents) {
|
||||
val calories = document.getLong("calories")?.toInt() ?: 0
|
||||
val date = document.getString("date") ?: "Unknown"
|
||||
val imagePath = document.getString("imagePath") ?: ""
|
||||
|
||||
val caloryData = CaloryModel(
|
||||
calories = calories,
|
||||
date = date,
|
||||
username = username,
|
||||
imagePath = imagePath
|
||||
)
|
||||
calorieList.add(caloryData)
|
||||
Log.d(TAG, "Kalori: $calories, Tanggal: $date, Image: $imagePath")
|
||||
}
|
||||
onComplete(calorieList)
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Gagal mengambil data kalori: ${e.message}")
|
||||
onComplete(emptyList())
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Pengguna tidak ditemukan: $username")
|
||||
onComplete(emptyList())
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Gagal mencari pengguna: ${e.message}")
|
||||
onComplete(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
// Menyimpan data kalori dengan gambar
|
||||
fun saveCaloryData(
|
||||
context: Context,
|
||||
bitmap: Bitmap,
|
||||
calories: Int,
|
||||
username: String,
|
||||
onComplete: (Boolean, String?) -> Unit
|
||||
) {
|
||||
// 1. Simpan gambar ke penyimpanan lokal
|
||||
val imagePath = LocalImageStorage.saveImageToInternalStorage(context, bitmap)
|
||||
|
||||
if (imagePath.isEmpty()) {
|
||||
onComplete(false, "Gagal menyimpan gambar")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Format tanggal saat ini (hanya tanggal, tanpa waktu)
|
||||
val currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
.format(Date())
|
||||
|
||||
// 3. Siapkan data kalori untuk disimpan
|
||||
val caloryData = hashMapOf(
|
||||
"calories" to calories,
|
||||
"date" to currentDate,
|
||||
"username" to username,
|
||||
"imagePath" to imagePath
|
||||
)
|
||||
|
||||
// 4. Simpan ke Firestore sesuai struktur database Anda
|
||||
db.collection("users")
|
||||
.whereEqualTo("username", username)
|
||||
.get()
|
||||
.addOnSuccessListener { result ->
|
||||
if (!result.isEmpty) {
|
||||
val userDocument = result.documents[0]
|
||||
val calorieId = db.collection("users")
|
||||
.document(userDocument.id)
|
||||
.collection("calorieData")
|
||||
.document().id
|
||||
|
||||
userDocument.reference.collection("calorieData")
|
||||
.document(calorieId)
|
||||
.set(caloryData)
|
||||
.addOnSuccessListener {
|
||||
Log.d(TAG, "Data kalori berhasil disimpan dengan ID: $calorieId")
|
||||
onComplete(true, calorieId)
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Gagal menyimpan data: ${e.message}")
|
||||
// Jika gagal menyimpan ke Firestore, hapus gambar lokal
|
||||
LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||
onComplete(false, e.message)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Pengguna tidak ditemukan: $username")
|
||||
// Hapus gambar jika user tidak ditemukan
|
||||
LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||
onComplete(false, "Pengguna tidak ditemukan")
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Gagal mencari pengguna: ${e.message}")
|
||||
// Hapus gambar jika terjadi error
|
||||
LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||
onComplete(false, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Menghapus data kalori berdasarkan tanggal dan kalori
|
||||
// Karena tidak ada ID dalam model, kita gunakan kombinasi tanggal+kalori untuk identifikasi
|
||||
fun deleteCaloryData(date: String, calories: Int, imagePath: String, username: String, onComplete: (Boolean) -> Unit) {
|
||||
db.collection("users")
|
||||
.whereEqualTo("username", username)
|
||||
.get()
|
||||
.addOnSuccessListener { result ->
|
||||
if (!result.isEmpty) {
|
||||
val userDocument = result.documents[0]
|
||||
|
||||
// Cari dokumen dengan tanggal dan kalori yang sesuai
|
||||
userDocument.reference.collection("calorieData")
|
||||
.whereEqualTo("date", date)
|
||||
.whereEqualTo("calories", calories)
|
||||
.get()
|
||||
.addOnSuccessListener { snapshot ->
|
||||
if (snapshot.documents.isNotEmpty()) {
|
||||
// Ambil dokumen pertama yang cocok
|
||||
val document = snapshot.documents[0]
|
||||
|
||||
// Hapus dokumen tersebut
|
||||
document.reference.delete()
|
||||
.addOnSuccessListener {
|
||||
Log.d(TAG, "Data kalori berhasil dihapus: $date, $calories")
|
||||
// Hapus gambar dari penyimpanan lokal
|
||||
val isDeleted = LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||
Log.d(TAG, "Gambar berhasil dihapus: $isDeleted")
|
||||
onComplete(true)
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Gagal menghapus data: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Data kalori tidak ditemukan: $date, $calories")
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Gagal mencari data kalori: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Pengguna tidak ditemukan: $username")
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Gagal mencari pengguna: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Membersihkan gambar yang tidak digunakan
|
||||
fun cleanupUnusedImages(context: Context, username: String) {
|
||||
// Dapatkan semua gambar yang digunakan
|
||||
getCalorieData(username) { allCalories ->
|
||||
val usedImagePaths = allCalories.map { it.imagePath }.filter { it.isNotEmpty() }
|
||||
// Bersihkan gambar yang tidak digunakan
|
||||
LocalImageStorage.cleanupUnusedImages(context, usedImagePaths)
|
||||
}
|
||||
}
|
||||
|
||||
// fun saveCalorieData(username: String, calories: Int, onComplete: (Boolean) -> Unit) {
|
||||
// val currentDate =
|
||||
// java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
// .format(java.util.Date())
|
||||
// val calorieData = hashMapOf(
|
||||
// "calories" to calories,
|
||||
// "date" to currentDate
|
||||
// )
|
||||
// val caloryData = CaloryModel(calories, currentDate, username)
|
||||
// db.collection("users")
|
||||
// .whereEqualTo("username", username)
|
||||
// .get()
|
||||
// .addOnSuccessListener { result ->
|
||||
// if (!result.isEmpty) {
|
||||
// val userDocument = result.documents[0]
|
||||
// val calorieId = db.collection("users")
|
||||
// .document(userDocument.id)
|
||||
// .collection("calorieData")
|
||||
// .document().id
|
||||
//
|
||||
// userDocument.reference.collection("calorieData")
|
||||
// .document(calorieId)
|
||||
// .set(caloryData)
|
||||
// .addOnSuccessListener {
|
||||
// Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||
// onComplete(true)
|
||||
// }
|
||||
// .addOnFailureListener { e ->
|
||||
// Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||
// onComplete(false)
|
||||
// }
|
||||
// } else {
|
||||
// Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||
// onComplete(false)
|
||||
// }
|
||||
// }
|
||||
// .addOnFailureListener { e ->
|
||||
// Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||
// onComplete(false)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// private val db = FirebaseFirestore.getInstance()
|
||||
// private val TAG = "CaloryRepository"
|
||||
|
||||
/**
|
||||
* Menyimpan hasil deteksi makanan ke Firestore (tanpa gambar)
|
||||
|
@ -36,7 +265,7 @@ class CaloryRepository {
|
|||
val weightGrams = 500 * percentage
|
||||
val categoryEnum = FoodCategory.values().find { it.displayName == category }
|
||||
val calories = categoryEnum?.let {
|
||||
(weightGrams * it.caloriesPer100g / 100).toInt()
|
||||
(weightGrams * it.caloriesPerPorsi / 100).toInt()
|
||||
} ?: 0
|
||||
calories
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
package com.example.caloryapp.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.example.caloryapp.model.UserModel
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import com.google.firebase.firestore.Query
|
||||
|
||||
class UserRepository {
|
||||
private val db = FirebaseFirestore.getInstance()
|
||||
|
||||
|
||||
fun registerUser(user: UserModel, onComplete: (Boolean) -> Unit) {
|
||||
db.collection("users")
|
||||
.add(user)
|
||||
|
@ -50,11 +54,60 @@ class UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fun saveCalorieData(username: String, calories: Int, onComplete: (Boolean) -> Unit) {
|
||||
val currentDate =
|
||||
java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
.format(java.util.Date())
|
||||
val calorieData = hashMapOf(
|
||||
"calories" to calories,
|
||||
"date" to currentDate
|
||||
)
|
||||
val caloryData = CaloryModel(calories, currentDate, username)
|
||||
db.collection("users")
|
||||
.whereEqualTo("username", username)
|
||||
.get()
|
||||
.addOnSuccessListener { result ->
|
||||
if (!result.isEmpty) {
|
||||
val userDocument = result.documents[0]
|
||||
val calorieId = db.collection("users")
|
||||
.document(userDocument.id)
|
||||
.collection("calorieData")
|
||||
.document().id
|
||||
|
||||
userDocument.reference.collection("calorieData")
|
||||
.document(calorieId)
|
||||
.set(caloryData)
|
||||
.addOnSuccessListener {
|
||||
Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||
onComplete(true)
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
} else {
|
||||
Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun logoutUser() {
|
||||
FirebaseAuth.getInstance().signOut()
|
||||
}
|
||||
|
||||
fun updatePasswordByUsername(username: String, oldPassword: String, newPassword: String, onComplete: (Boolean) -> Unit) {
|
||||
fun updatePasswordByUsername(
|
||||
username: String,
|
||||
oldPassword: String,
|
||||
newPassword: String,
|
||||
onComplete: (Boolean) -> Unit
|
||||
) {
|
||||
db.collection("users")
|
||||
.whereEqualTo("username", username) // Mencari pengguna berdasarkan username
|
||||
.get()
|
||||
|
@ -85,7 +138,15 @@ class UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateUserData(username: String, fullName: String, email: String, gender: String, weight: String, height: String, onComplete: (Boolean) -> Unit) {
|
||||
fun updateUserData(
|
||||
username: String,
|
||||
fullName: String,
|
||||
email: String,
|
||||
gender: String,
|
||||
weight: String,
|
||||
height: String,
|
||||
onComplete: (Boolean) -> Unit
|
||||
) {
|
||||
db.collection("users")
|
||||
.whereEqualTo("username", username)
|
||||
.get()
|
||||
|
@ -116,7 +177,12 @@ class UserRepository {
|
|||
}
|
||||
|
||||
|
||||
fun updatePasswordByUsername2(username: String, newPassword: String, confirmPassword: String, onComplete: (Boolean) -> Unit) {
|
||||
fun updatePasswordByUsername2(
|
||||
username: String,
|
||||
newPassword: String,
|
||||
confirmPassword: String,
|
||||
onComplete: (Boolean) -> Unit
|
||||
) {
|
||||
// Memeriksa apakah kata sandi baru dan konfirmasi cocok
|
||||
if (newPassword != confirmPassword) {
|
||||
onComplete(false) // Kata sandi tidak cocok
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.example.caloryapp.ui.theme
|
|||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
var primary = Color(0xff109A17)
|
||||
var primary2 = Color(0xff6bbe6f)
|
||||
var primaryblack = Color(0xff202020)
|
||||
var primarygrey = Color(0xffBABABA)
|
||||
var background = Color(0xffF4F4F4)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.example.caloryapp.utils
|
||||
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
|
||||
object CaloryCalculator {
|
||||
// Kebutuhan kalori harian berdasarkan gender
|
||||
const val MALE_DAILY_CALORY_NEEDS = 2650
|
||||
const val FEMALE_DAILY_CALORY_NEEDS = 2250
|
||||
|
||||
/**
|
||||
* Menghitung kebutuhan kalori berdasarkan gender
|
||||
*/
|
||||
fun getDailyCaloryNeeds(gender: String): Int {
|
||||
return when (gender.lowercase()) {
|
||||
"male", "pria", "laki-laki" -> MALE_DAILY_CALORY_NEEDS
|
||||
"female", "wanita", "perempuan" -> FEMALE_DAILY_CALORY_NEEDS
|
||||
else -> 2400 // Default jika gender tidak diketahui
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghitung persentase konsumsi kalori terhadap kebutuhan harian
|
||||
*/
|
||||
fun calculateDailyPercentage(consumedCalories: Int, gender: String): Float {
|
||||
val dailyNeeds = getDailyCaloryNeeds(gender)
|
||||
return (consumedCalories.toFloat() / dailyNeeds) * 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghitung total kalori yang dikonsumsi pada hari ini
|
||||
*/
|
||||
fun calculateTodayTotalCalories(caloriesList: List<CaloryModel>): Int {
|
||||
// Filter kalori hari ini
|
||||
val today = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
|
||||
.format(java.util.Date())
|
||||
|
||||
// Jumlahkan semua kalori hari ini
|
||||
return caloriesList
|
||||
.filter { it.date == today }
|
||||
.sumOf { it.calories }
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghitung sisa kalori yang dapat dikonsumsi
|
||||
*/
|
||||
fun calculateRemainingCalories(consumedCalories: Int, gender: String): Int {
|
||||
val dailyNeeds = getDailyCaloryNeeds(gender)
|
||||
val remaining = dailyNeeds - consumedCalories
|
||||
return if (remaining < 0) 0 else remaining
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package com.example.caloryapp.utils
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object LocalImageStorage {
|
||||
private const val TAG = "LocalImageStorage"
|
||||
private const val IMAGE_DIRECTORY = "calory_images"
|
||||
|
||||
// Menyimpan gambar ke penyimpanan internal
|
||||
fun saveImageToInternalStorage(context: Context, bitmap: Bitmap, fileName: String? = null): String {
|
||||
// Buat nama file unik jika tidak disediakan
|
||||
val imageName = fileName ?: "IMG_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())}.jpg"
|
||||
|
||||
// Buat direktori jika belum ada
|
||||
val directory = File(context.filesDir, IMAGE_DIRECTORY)
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Path file lengkap
|
||||
val file = File(directory, imageName)
|
||||
|
||||
try {
|
||||
// Simpan bitmap ke file
|
||||
FileOutputStream(file).use { stream ->
|
||||
// Kompres gambar dengan kualitas 85% untuk menghemat ruang
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, stream)
|
||||
stream.flush()
|
||||
}
|
||||
Log.d(TAG, "Gambar berhasil disimpan: ${file.absolutePath}")
|
||||
return file.absolutePath
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Gagal menyimpan gambar: ${e.message}")
|
||||
e.printStackTrace()
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Mengambil gambar dari penyimpanan internal
|
||||
fun getImageFromInternalStorage(context: Context, imagePath: String): Bitmap? {
|
||||
return try {
|
||||
val file = File(imagePath)
|
||||
if (file.exists()) {
|
||||
BitmapFactory.decodeFile(file.absolutePath)
|
||||
} else {
|
||||
Log.w(TAG, "File tidak ditemukan: $imagePath")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Gagal mengambil gambar: ${e.message}")
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus gambar dari penyimpanan internal
|
||||
fun deleteImageFromInternalStorage(imagePath: String): Boolean {
|
||||
return try {
|
||||
val file = File(imagePath)
|
||||
if (file.exists()) {
|
||||
val result = file.delete()
|
||||
Log.d(TAG, if (result) "Gambar berhasil dihapus: $imagePath" else "Gagal menghapus gambar: $imagePath")
|
||||
result
|
||||
} else {
|
||||
Log.w(TAG, "File tidak ditemukan untuk dihapus: $imagePath")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error saat menghapus gambar: ${e.message}")
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Mendapatkan URI untuk dibagikan (FileProvider)
|
||||
fun getUriForImage(context: Context, imagePath: String): Uri? {
|
||||
val file = File(imagePath)
|
||||
return if (file.exists()) {
|
||||
try {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error saat mendapatkan URI: ${e.message}")
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "File tidak ditemukan untuk URI: $imagePath")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Bersihkan gambar yang sudah tidak digunakan
|
||||
fun cleanupUnusedImages(context: Context, usedImagePaths: List<String>) {
|
||||
try {
|
||||
val directory = File(context.filesDir, IMAGE_DIRECTORY)
|
||||
if (directory.exists()) {
|
||||
val files = directory.listFiles()
|
||||
files?.forEach { file ->
|
||||
if (!usedImagePaths.contains(file.absolutePath)) {
|
||||
val isDeleted = file.delete()
|
||||
Log.d(TAG, "Membersihkan gambar tidak terpakai: ${file.name}, berhasil: $isDeleted")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error saat membersihkan gambar: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package com.example.caloryapp.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
|
||||
class SessionManager(context: Context) {
|
||||
private val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences("CaloryAppSession", Context.MODE_PRIVATE)
|
||||
private val editor: SharedPreferences.Editor = sharedPreferences.edit()
|
||||
|
||||
companion object {
|
||||
private const val KEY_IS_LOGGED_IN = "isLoggedIn"
|
||||
private const val KEY_USERNAME = "username"
|
||||
private const val KEY_FULL_NAME = "fullName"
|
||||
private const val KEY_EMAIL = "email"
|
||||
private const val KEY_GENDER = "gender"
|
||||
private const val KEY_WEIGHT = "weight"
|
||||
private const val KEY_HEIGHT = "height"
|
||||
}
|
||||
|
||||
// Menyimpan sesi login dengan penanganan null
|
||||
fun saveLoginSession(user: com.example.caloryapp.model.UserModel) {
|
||||
try {
|
||||
editor.putBoolean(KEY_IS_LOGGED_IN, true)
|
||||
editor.putString(KEY_USERNAME, user.username ?: "")
|
||||
editor.putString(KEY_FULL_NAME, user.fullName ?: "")
|
||||
editor.putString(KEY_EMAIL, user.email ?: "")
|
||||
editor.putString(KEY_GENDER, user.gender ?: "")
|
||||
editor.putString(KEY_WEIGHT, user.weight ?: "")
|
||||
editor.putString(KEY_HEIGHT, user.height ?: "")
|
||||
editor.apply()
|
||||
Log.d("SessionManager", "Session saved successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("SessionManager", "Error saving session: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Mengecek apakah user sudah login
|
||||
fun isLoggedIn(): Boolean {
|
||||
return sharedPreferences.getBoolean(KEY_IS_LOGGED_IN, false)
|
||||
}
|
||||
|
||||
// Mendapatkan data user yang tersimpan
|
||||
fun getUserData(): com.example.caloryapp.model.UserModel {
|
||||
return com.example.caloryapp.model.UserModel(
|
||||
username = sharedPreferences.getString(KEY_USERNAME, "") ?: "",
|
||||
fullName = sharedPreferences.getString(KEY_FULL_NAME, "") ?: "",
|
||||
email = sharedPreferences.getString(KEY_EMAIL, "") ?: "",
|
||||
password = "", // Password tidak disimpan di SharedPreferences untuk keamanan
|
||||
gender = sharedPreferences.getString(KEY_GENDER, "") ?: "",
|
||||
weight = sharedPreferences.getString(KEY_WEIGHT, "") ?: "",
|
||||
height = sharedPreferences.getString(KEY_HEIGHT, "") ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
// Menghapus sesi saat logout dengan try-catch
|
||||
fun clearSession() {
|
||||
try {
|
||||
editor.clear()
|
||||
editor.apply()
|
||||
Log.d("SessionManager", "Session cleared successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("SessionManager", "Error clearing session: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,129 +1,262 @@
|
|||
package com.example.caloryapp.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.caloryapp.model.CaloryHistoryModel
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.example.caloryapp.repository.CaloryRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.example.caloryapp.utils.LocalImageStorage
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* ViewModel untuk mengelola riwayat kalori
|
||||
*/
|
||||
class CaloryHistoryViewModel : ViewModel() {
|
||||
private val TAG = "CaloryHistoryViewModel"
|
||||
private val repository = CaloryRepository()
|
||||
|
||||
private val caloryRepository = CaloryRepository()
|
||||
private val _calorieList = mutableStateOf<List<CaloryModel>>(emptyList())
|
||||
val calorieList: State<List<CaloryModel>> = _calorieList
|
||||
|
||||
// State untuk UI
|
||||
var isLoading by mutableStateOf(false)
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
private val _isLoading = mutableStateOf(false)
|
||||
val isLoading: State<Boolean> = _isLoading
|
||||
|
||||
// List riwayat kalori
|
||||
var historyList by mutableStateOf<List<CaloryHistoryModel>>(emptyList())
|
||||
private val _error = mutableStateOf<String?>(null)
|
||||
val error: State<String?> = _error
|
||||
|
||||
// Detail riwayat kalori yang dipilih
|
||||
var selectedHistory by mutableStateOf<CaloryHistoryModel?>(null)
|
||||
// Menyimpan data kalori baru
|
||||
fun saveCaloryData(
|
||||
context: Context,
|
||||
bitmap: Bitmap,
|
||||
calories: Int,
|
||||
username: String,
|
||||
onComplete: (Boolean, String?) -> Unit
|
||||
) {
|
||||
_isLoading.value = true
|
||||
_error.value = null
|
||||
|
||||
/**
|
||||
* Memuat riwayat kalori berdasarkan username
|
||||
*/
|
||||
fun loadHistoryByUsername(username: String, limit: Long = 10) {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
// Menggunakan viewModelScope, yang merupakan CoroutineScope yang otomatis dibatalkan saat ViewModel dihancurkan
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// Membungkus fungsi callback-based dalam withContext untuk menjalankannya di thread IO
|
||||
withContext(Dispatchers.IO) {
|
||||
caloryRepository.getCaloryHistoryByUsername(username, limit) { histories ->
|
||||
// Kembali ke thread utama untuk memperbarui UI
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
historyList = histories
|
||||
isLoading = false
|
||||
repository.saveCaloryData(
|
||||
context,
|
||||
bitmap,
|
||||
calories,
|
||||
username
|
||||
) { success, message ->
|
||||
_isLoading.value = false
|
||||
if (!success) {
|
||||
_error.value = message ?: "Gagal menyimpan data"
|
||||
} else {
|
||||
// Refresh list setelah berhasil menambahkan
|
||||
loadHistoryByUsername(username)
|
||||
}
|
||||
onComplete(success, message)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
errorMessage = "Gagal memuat riwayat kalori: ${e.message}"
|
||||
isLoading = false
|
||||
}
|
||||
Log.e(TAG, "Error saat menyimpan data: ${e.message}")
|
||||
_isLoading.value = false
|
||||
_error.value = e.message
|
||||
onComplete(false, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memuat detail riwayat kalori berdasarkan ID
|
||||
*/
|
||||
fun loadHistoryDetail(id: String) {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
// Memuat data kalori untuk user
|
||||
fun loadHistoryByUsername(username: String, limit: Int = 0) {
|
||||
_isLoading.value = true
|
||||
_error.value = null
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
caloryRepository.getCaloryHistoryById(id) { history ->
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
selectedHistory = history
|
||||
isLoading = false
|
||||
repository.getCalorieData(username) { data ->
|
||||
_calorieList.value = if (limit > 0 && data.size > limit) {
|
||||
data.take(limit)
|
||||
} else {
|
||||
data
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error saat memuat data: ${e.message}")
|
||||
_isLoading.value = false
|
||||
_error.value = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (history == null) {
|
||||
errorMessage = "Riwayat kalori tidak ditemukan"
|
||||
// Hapus data kalori
|
||||
fun deleteCaloryData(date: String, calories: Int, imagePath: String, username: String, onComplete: (Boolean) -> Unit) {
|
||||
_isLoading.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
repository.deleteCaloryData(date, calories, imagePath, username) { success ->
|
||||
_isLoading.value = false
|
||||
if (success) {
|
||||
// Update list setelah penghapusan
|
||||
_calorieList.value = _calorieList.value.filter {
|
||||
it.date != date || it.calories != calories
|
||||
}
|
||||
}
|
||||
onComplete(success)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
errorMessage = "Gagal memuat detail riwayat: ${e.message}"
|
||||
isLoading = false
|
||||
}
|
||||
Log.e(TAG, "Error saat menghapus data: ${e.message}")
|
||||
_isLoading.value = false
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghapus riwayat kalori
|
||||
*/
|
||||
fun deleteHistory(id: String, onComplete: (Boolean) -> Unit) {
|
||||
isLoading = true
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
caloryRepository.deleteCaloryHistory(id) { success ->
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
isLoading = false
|
||||
if (success) {
|
||||
// Hapus dari list lokal jika berhasil
|
||||
historyList = historyList.filter { it.id != id }
|
||||
}
|
||||
onComplete(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
errorMessage = "Gagal menghapus riwayat: ${e.message}"
|
||||
isLoading = false
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mendapatkan bitmap dari path gambar
|
||||
fun getImageBitmap(context: Context, imagePath: String): Bitmap? {
|
||||
if (imagePath.isEmpty()) return null
|
||||
return LocalImageStorage.getImageFromInternalStorage(context, imagePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state
|
||||
*/
|
||||
fun resetState() {
|
||||
historyList = emptyList()
|
||||
selectedHistory = null
|
||||
errorMessage = null
|
||||
isLoading = false
|
||||
// Membersihkan cache gambar yang tidak digunakan
|
||||
fun cleanupUnusedImages(context: Context, username: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.cleanupUnusedImages(context, username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//package com.example.caloryapp.viewmodel
|
||||
//
|
||||
//import androidx.compose.runtime.getValue
|
||||
//import androidx.compose.runtime.mutableStateOf
|
||||
//import androidx.compose.runtime.setValue
|
||||
//import androidx.lifecycle.ViewModel
|
||||
//import androidx.lifecycle.viewModelScope
|
||||
//import com.example.caloryapp.model.CaloryHistoryModel
|
||||
//import com.example.caloryapp.repository.CaloryRepository
|
||||
//import kotlinx.coroutines.Dispatchers
|
||||
//import kotlinx.coroutines.launch
|
||||
//import kotlinx.coroutines.withContext
|
||||
//
|
||||
///**
|
||||
// * ViewModel untuk mengelola riwayat kalori
|
||||
// */
|
||||
//class CaloryHistoryViewModel : ViewModel() {
|
||||
//
|
||||
// private val caloryRepository = CaloryRepository()
|
||||
//
|
||||
// // State untuk UI
|
||||
// var isLoading by mutableStateOf(false)
|
||||
// var errorMessage by mutableStateOf<String?>(null)
|
||||
//
|
||||
// // List riwayat kalori
|
||||
// var historyList by mutableStateOf<List<CaloryHistoryModel>>(emptyList())
|
||||
//
|
||||
// // Detail riwayat kalori yang dipilih
|
||||
// var selectedHistory by mutableStateOf<CaloryHistoryModel?>(null)
|
||||
//
|
||||
// /**
|
||||
// * Memuat riwayat kalori berdasarkan username
|
||||
// */
|
||||
// fun loadHistoryByUsername(username: String, limit: Long = 10) {
|
||||
// isLoading = true
|
||||
// errorMessage = null
|
||||
//
|
||||
// // Menggunakan viewModelScope, yang merupakan CoroutineScope yang otomatis dibatalkan saat ViewModel dihancurkan
|
||||
// viewModelScope.launch {
|
||||
// try {
|
||||
// // Membungkus fungsi callback-based dalam withContext untuk menjalankannya di thread IO
|
||||
// withContext(Dispatchers.IO) {
|
||||
// caloryRepository.getCaloryHistoryByUsername(username, limit) { histories ->
|
||||
// // Kembali ke thread utama untuk memperbarui UI
|
||||
// viewModelScope.launch(Dispatchers.Main) {
|
||||
// historyList = histories
|
||||
// isLoading = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// withContext(Dispatchers.Main) {
|
||||
// errorMessage = "Gagal memuat riwayat kalori: ${e.message}"
|
||||
// isLoading = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Memuat detail riwayat kalori berdasarkan ID
|
||||
// */
|
||||
// fun loadHistoryDetail(id: String) {
|
||||
// isLoading = true
|
||||
// errorMessage = null
|
||||
//
|
||||
// viewModelScope.launch {
|
||||
// try {
|
||||
// withContext(Dispatchers.IO) {
|
||||
// caloryRepository.getCaloryHistoryById(id) { history ->
|
||||
// viewModelScope.launch(Dispatchers.Main) {
|
||||
// selectedHistory = history
|
||||
// isLoading = false
|
||||
//
|
||||
// if (history == null) {
|
||||
// errorMessage = "Riwayat kalori tidak ditemukan"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// withContext(Dispatchers.Main) {
|
||||
// errorMessage = "Gagal memuat detail riwayat: ${e.message}"
|
||||
// isLoading = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Menghapus riwayat kalori
|
||||
// */
|
||||
// fun deleteHistory(id: String, onComplete: (Boolean) -> Unit) {
|
||||
// isLoading = true
|
||||
//
|
||||
// viewModelScope.launch {
|
||||
// try {
|
||||
// withContext(Dispatchers.IO) {
|
||||
// caloryRepository.deleteCaloryHistory(id) { success ->
|
||||
// viewModelScope.launch(Dispatchers.Main) {
|
||||
// isLoading = false
|
||||
// if (success) {
|
||||
// // Hapus dari list lokal jika berhasil
|
||||
// historyList = historyList.filter { it.id != id }
|
||||
// }
|
||||
// onComplete(success)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// withContext(Dispatchers.Main) {
|
||||
// errorMessage = "Gagal menghapus riwayat: ${e.message}"
|
||||
// isLoading = false
|
||||
// onComplete(false)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Reset state
|
||||
// */
|
||||
// fun resetState() {
|
||||
// historyList = emptyList()
|
||||
// selectedHistory = null
|
||||
// errorMessage = null
|
||||
// isLoading = false
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,68 @@
|
|||
package com.example.caloryapp.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CaloryViewModel : ViewModel() {
|
||||
|
||||
private val db = FirebaseFirestore.getInstance()
|
||||
|
||||
// Fungsi untuk menyimpan data kalori
|
||||
fun saveCaloryData(calories: Int, onComplete: (Boolean) -> Unit) {
|
||||
val currentUser = FirebaseAuth.getInstance().currentUser
|
||||
if (currentUser == null) {
|
||||
Log.e("CaloryApp", "Pengguna belum login")
|
||||
onComplete(false)
|
||||
return
|
||||
}
|
||||
|
||||
val username = currentUser.displayName ?: "Unknown"
|
||||
val currentDate = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
|
||||
val caloryData = CaloryModel(calories, currentDate, username)
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
db.collection("users")
|
||||
.whereEqualTo("username", username)
|
||||
.get()
|
||||
.addOnSuccessListener { result ->
|
||||
if (!result.isEmpty) {
|
||||
val userDocument = result.documents[0]
|
||||
val calorieId = db.collection("users")
|
||||
.document(userDocument.id)
|
||||
.collection("calorieData")
|
||||
.document().id
|
||||
|
||||
userDocument.reference.collection("calorieData")
|
||||
.document(calorieId)
|
||||
.set(caloryData)
|
||||
.addOnSuccessListener {
|
||||
Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||
onComplete(true)
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
} else {
|
||||
Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("CaloryApp", "Error: ${e.message}")
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -138,8 +138,10 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.caloryapp.foodmodel.FoodCategory
|
||||
import com.example.caloryapp.foodmodel.FoodDetectionResult
|
||||
import com.example.caloryapp.foodmodel.FoodDetector
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -155,24 +157,55 @@ class FoodDetectionViewModel : ViewModel() {
|
|||
var errorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
var isSaving by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var saveSuccess by mutableStateOf<Boolean?>(null)
|
||||
private set
|
||||
|
||||
// Ambang batas confidence untuk kategori utama
|
||||
private val MAIN_CATEGORY_THRESHOLD = 0.35f
|
||||
|
||||
// Rasio minimum antara kategori utama dan kategori kedua
|
||||
// Ini membantu memastikan bahwa model cukup yakin tentang kategori utama
|
||||
private val CONFIDENCE_RATIO_THRESHOLD = 1.5f
|
||||
|
||||
fun detectFoodFromImage(context: Context, uri: Uri) {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
detectionResult = null
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val bitmap = loadBitmapFromUri(context, uri)
|
||||
|
||||
// Buat detector di sini, tidak dalam withContext
|
||||
// Buat detector
|
||||
val detector = FoodDetector(context)
|
||||
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
detector.detectFood(bitmap)
|
||||
}
|
||||
detectionResult = result
|
||||
|
||||
// Log hasil deteksi untuk debugging
|
||||
Log.d("FoodViewModel", "Detection result: ${result.mainCategory.name}, " +
|
||||
"confidence: ${result.confidence}, " +
|
||||
"all categories: ${result.allCategories.entries.joinToString { "${it.key.name}=${it.value}" }}")
|
||||
|
||||
// Periksa dengan metode yang lebih canggih
|
||||
if (isFoodImageAdvanced(result)) {
|
||||
detectionResult = result
|
||||
errorMessage = null
|
||||
} else {
|
||||
detectionResult = null
|
||||
errorMessage = "Pindai Makanan Gagal!"
|
||||
Log.d("FoodViewModel", "Image tidak lolos validasi makanan")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e("FoodViewModel", "Error IO: ${e.message}", e)
|
||||
errorMessage = "Gagal memuat gambar atau model: ${e.message}"
|
||||
} catch (e: OutOfMemoryError) {
|
||||
Log.e("FoodViewModel", "Out of memory: ${e.message}", e)
|
||||
errorMessage = "Gambar terlalu besar. Gunakan gambar dengan resolusi lebih rendah."
|
||||
} catch (e: Exception) {
|
||||
Log.e("FoodViewModel", "Error umum: ${e.message}", e)
|
||||
errorMessage = "Terjadi kesalahan: ${e.message}"
|
||||
|
@ -182,10 +215,180 @@ class FoodDetectionViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
// Metode yang lebih canggih untuk memeriksa apakah gambar adalah makanan
|
||||
private fun isFoodImageAdvanced(result: FoodDetectionResult): Boolean {
|
||||
// Confidence untuk kategori utama harus di atas threshold
|
||||
if (result.confidence < MAIN_CATEGORY_THRESHOLD) {
|
||||
Log.d("FoodViewModel", "Main category confidence too low: ${result.confidence} < $MAIN_CATEGORY_THRESHOLD")
|
||||
return false
|
||||
}
|
||||
|
||||
// Dapatkan nilai confidence untuk semua kategori dan urutkan dari tertinggi ke terendah
|
||||
val sortedConfidences = result.allCategories.values.sortedDescending()
|
||||
|
||||
// Jika hanya ada satu kategori, gunakan threshold normal
|
||||
if (sortedConfidences.size <= 1) {
|
||||
return result.confidence >= MAIN_CATEGORY_THRESHOLD
|
||||
}
|
||||
|
||||
// Dapatkan confidence untuk kategori utama dan kategori kedua tertinggi
|
||||
val topConfidence = sortedConfidences[0]
|
||||
val secondConfidence = sortedConfidences[1]
|
||||
|
||||
// Hitung rasio antara kategori utama dan kategori kedua
|
||||
// Ini membantu menentukan apakah model benar-benar yakin tentang kategori utama
|
||||
// dibandingkan dengan kategori lainnya
|
||||
val confidenceRatio = if (secondConfidence > 0) topConfidence / secondConfidence else Float.MAX_VALUE
|
||||
|
||||
// Log untuk debugging
|
||||
Log.d("FoodViewModel", "Top confidence: $topConfidence, Second confidence: $secondConfidence, Ratio: $confidenceRatio")
|
||||
|
||||
// Kategori utama harus memiliki confidence yang cukup tinggi
|
||||
// dan rasio confidence harus di atas threshold
|
||||
val isFood = topConfidence >= MAIN_CATEGORY_THRESHOLD && confidenceRatio >= CONFIDENCE_RATIO_THRESHOLD
|
||||
|
||||
Log.d("FoodViewModel", "Is food: $isFood")
|
||||
return isFood
|
||||
}
|
||||
|
||||
private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap {
|
||||
val contentResolver = context.contentResolver
|
||||
return contentResolver.openInputStream(uri).use { inputStream ->
|
||||
android.graphics.BitmapFactory.decodeStream(inputStream)
|
||||
} ?: throw IOException("Gagal membuka gambar")
|
||||
}
|
||||
|
||||
// Fungsi untuk menyimpan hasil deteksi (kode tidak berubah)
|
||||
fun saveDetectionResult(username: String) {
|
||||
// ... kode yang sama seperti sebelumnya
|
||||
}
|
||||
|
||||
// Fungsi untuk mereset state
|
||||
fun resetState() {
|
||||
isLoading = false
|
||||
isSaving = false
|
||||
detectionResult = null
|
||||
errorMessage = null
|
||||
saveSuccess = null
|
||||
}
|
||||
}
|
||||
|
||||
// kode benar
|
||||
//package com.example.caloryapp.viewmodel
|
||||
//
|
||||
//import android.content.Context
|
||||
//import android.graphics.Bitmap
|
||||
//import android.net.Uri
|
||||
//import android.util.Log
|
||||
//import androidx.compose.runtime.getValue
|
||||
//import androidx.compose.runtime.mutableStateOf
|
||||
//import androidx.compose.runtime.setValue
|
||||
//import androidx.lifecycle.ViewModel
|
||||
//import androidx.lifecycle.viewModelScope
|
||||
//import com.example.caloryapp.foodmodel.FoodDetectionResult
|
||||
//import com.example.caloryapp.foodmodel.FoodDetector
|
||||
//import com.google.firebase.firestore.FirebaseFirestore
|
||||
//import kotlinx.coroutines.Dispatchers
|
||||
//import kotlinx.coroutines.launch
|
||||
//import kotlinx.coroutines.withContext
|
||||
//import java.io.IOException
|
||||
//
|
||||
//class FoodDetectionViewModel : ViewModel() {
|
||||
// var detectionResult by mutableStateOf<FoodDetectionResult?>(null)
|
||||
// private set
|
||||
//
|
||||
// var isLoading by mutableStateOf(false)
|
||||
// private set
|
||||
//
|
||||
// var errorMessage by mutableStateOf<String?>(null)
|
||||
// private set
|
||||
//
|
||||
// fun detectFoodFromImage(context: Context, uri: Uri) {
|
||||
// isLoading = true
|
||||
// errorMessage = null
|
||||
//
|
||||
// viewModelScope.launch {
|
||||
// try {
|
||||
// val bitmap = loadBitmapFromUri(context, uri)
|
||||
//
|
||||
// // Buat detector di sini, tidak dalam withContext
|
||||
// val detector = FoodDetector(context)
|
||||
//
|
||||
// val result = withContext(Dispatchers.IO) {
|
||||
// detector.detectFood(bitmap)
|
||||
// }
|
||||
// detectionResult = result
|
||||
// } catch (e: IOException) {
|
||||
// Log.e("FoodViewModel", "Error IO: ${e.message}", e)
|
||||
// errorMessage = "Gagal memuat gambar atau model: ${e.message}"
|
||||
// } catch (e: Exception) {
|
||||
// Log.e("FoodViewModel", "Error umum: ${e.message}", e)
|
||||
// errorMessage = "Terjadi kesalahan: ${e.message}"
|
||||
// } finally {
|
||||
// isLoading = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap {
|
||||
// val contentResolver = context.contentResolver
|
||||
// return contentResolver.openInputStream(uri).use { inputStream ->
|
||||
// android.graphics.BitmapFactory.decodeStream(inputStream)
|
||||
// } ?: throw IOException("Gagal membuka gambar")
|
||||
// }
|
||||
//}
|
||||
|
||||
// // Fungsi untuk menyimpan data kalori
|
||||
// fun saveCaloryData(calories: Int, onComplete: (Boolean) -> Unit) {
|
||||
// val currentUser = FirebaseAuth.getInstance().currentUser
|
||||
// if (currentUser == null) {
|
||||
// Log.e("CaloryApp", "Pengguna belum login")
|
||||
// onComplete(false)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// val username = currentUser.displayName ?: "Unknown"
|
||||
// val currentDate = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
//
|
||||
// val caloryData = CaloryModel(calories, currentDate, username)
|
||||
//
|
||||
// viewModelScope.launch {
|
||||
// try {
|
||||
// db.collection("users")
|
||||
// .whereEqualTo("username", username)
|
||||
// .get()
|
||||
// .addOnSuccessListener { result ->
|
||||
// if (!result.isEmpty) {
|
||||
// val userDocument = result.documents[0]
|
||||
// val calorieId = db.collection("users")
|
||||
// .document(userDocument.id)
|
||||
// .collection("calorieData")
|
||||
// .document().id
|
||||
//
|
||||
// userDocument.reference.collection("calorieData")
|
||||
// .document(calorieId)
|
||||
// .set(caloryData)
|
||||
// .addOnSuccessListener {
|
||||
// Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||
// onComplete(true)
|
||||
// }
|
||||
// .addOnFailureListener { e ->
|
||||
// Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||
// onComplete(false)
|
||||
// }
|
||||
// } else {
|
||||
// Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||
// onComplete(false)
|
||||
// }
|
||||
// }
|
||||
// .addOnFailureListener { e ->
|
||||
// Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||
// onComplete(false)
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// Log.e("CaloryApp", "Error: ${e.message}")
|
||||
// onComplete(false)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -1,5 +1,7 @@
|
|||
package com.example.caloryapp.viewmodel
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -7,6 +9,8 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.caloryapp.model.UserModel
|
||||
import com.example.caloryapp.repository.UserRepository
|
||||
import com.example.caloryapp.utils.SessionManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -15,41 +19,95 @@ sealed class LoginState {
|
|||
data object Loading : LoginState()
|
||||
data class Success(val user: UserModel) : LoginState()
|
||||
data class Error(val message: String) : LoginState()
|
||||
data object AlreadyLoggedIn : LoginState()
|
||||
data object LoggedOut : LoginState()// State baru untuk user yang sudah login
|
||||
}
|
||||
|
||||
class UserViewModel : ViewModel() {
|
||||
private val repository = UserRepository()
|
||||
private var sessionManager: SessionManager? = null
|
||||
|
||||
private val _registerState = MutableStateFlow<Boolean?>(null)
|
||||
val registerState: StateFlow<Boolean?> = _registerState
|
||||
|
||||
// Menyimpan data login state (loading, success, error)
|
||||
private val _loginstate = mutableStateOf<LoginState>(LoginState.Loading)
|
||||
val loginstate = _loginstate
|
||||
private val _loginState = mutableStateOf<LoginState>(LoginState.Loading)
|
||||
val loginState: State<LoginState> = _loginState
|
||||
|
||||
// Menyimpan data pengguna yang login
|
||||
private val _user = mutableStateOf<UserModel?>(null)
|
||||
val user = _user
|
||||
val user: State<UserModel?> = _user
|
||||
|
||||
// Inisialisasi SessionManager
|
||||
fun initSessionManager(context: Context) {
|
||||
sessionManager = SessionManager(context)
|
||||
// Cek apakah user sudah login sebelumnya
|
||||
if (sessionManager?.isLoggedIn() == true) {
|
||||
_user.value = sessionManager?.getUserData()
|
||||
_loginState.value = LoginState.AlreadyLoggedIn
|
||||
}
|
||||
}
|
||||
|
||||
// Cek apakah user sudah login
|
||||
fun checkLoginStatus(): Boolean {
|
||||
return sessionManager?.isLoggedIn() ?: false
|
||||
}
|
||||
|
||||
fun login(username: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
_loginstate.value = LoginState.Loading
|
||||
_loginState.value = LoginState.Loading
|
||||
// Fetch user by username from Firestore
|
||||
repository.getUserByUsername(username) { fetchedUser ->
|
||||
if (fetchedUser != null && fetchedUser.password == password) {
|
||||
_user.value = fetchedUser // Store user data in ViewModel
|
||||
_loginstate.value = LoginState.Success(fetchedUser)
|
||||
_loginState.value = LoginState.Success(fetchedUser)
|
||||
// Simpan sesi login
|
||||
sessionManager?.saveLoginSession(fetchedUser)
|
||||
Log.d("Login", "Fetched user: $fetchedUser")
|
||||
Log.d("Login", "User fullName: ${fetchedUser?.fullName}")
|
||||
Log.d("Login", "User username: ${fetchedUser?.username}")
|
||||
|
||||
Log.d("Login", "User fullName: ${fetchedUser.fullName}")
|
||||
Log.d("Login", "User username: ${fetchedUser.username}")
|
||||
} else {
|
||||
_loginstate.value = LoginState.Error("Invalid username or password")
|
||||
_loginState.value = LoginState.Error("Invalid username or password")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logout user
|
||||
fun logout() {
|
||||
try {
|
||||
// Gunakan repository untuk logout jika perlu
|
||||
// repository.logoutUser()
|
||||
|
||||
// Bersihkan session
|
||||
sessionManager?.clearSession()
|
||||
|
||||
// Reset state
|
||||
_user.value = null
|
||||
_loginState.value = LoginState.LoggedOut
|
||||
|
||||
viewModelScope.launch {
|
||||
delay(100) // Delay kecil
|
||||
// Baru hapus data user
|
||||
_user.value = null
|
||||
}
|
||||
|
||||
|
||||
Log.d("Logout", "User berhasil logout")
|
||||
} catch (e: Exception) {
|
||||
Log.e("Logout", "Error saat logout: ${e.message}")
|
||||
// Tetap set state ke LoggedOut meskipun ada error
|
||||
_loginState.value = LoginState.LoggedOut
|
||||
}
|
||||
// repository.logoutUser()
|
||||
// sessionManager?.clearSession()
|
||||
// _user.value = null
|
||||
// _loginState.value = LoginState.LoggedOut
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// **REGISTER USER**
|
||||
// fun register(email: String, password: String, user: UserModel) {
|
||||
// viewModelScope.launch {
|
||||
|
@ -80,4 +138,3 @@ class UserViewModel : ViewModel() {
|
|||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
package com.example.caloryapp.widget
|
||||
|
||||
|
||||
//package com.example.caloryapp.widget
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
@ -10,6 +14,7 @@ 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.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
|
@ -20,18 +25,32 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.caloryapp.model.CaloryHistoryModel
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.example.caloryapp.ui.theme.medium
|
||||
import com.example.caloryapp.ui.theme.primary
|
||||
import com.example.caloryapp.ui.theme.primary2
|
||||
import com.example.caloryapp.ui.theme.semibold
|
||||
import com.example.caloryapp.utils.LocalImageStorage
|
||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Komponen list riwayat kalori
|
||||
|
@ -40,8 +59,11 @@ import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
|||
fun CaloryHistoryList(
|
||||
viewModel: CaloryHistoryViewModel,
|
||||
username: String,
|
||||
onItemClick: (CaloryHistoryModel) -> Unit = {}
|
||||
onItemClick: (CaloryModel) -> Unit = {},
|
||||
onItemDelete: (CaloryModel) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// Muat data saat komponen pertama kali ditampilkan
|
||||
LaunchedEffect(username) {
|
||||
viewModel.loadHistoryByUsername(username)
|
||||
|
@ -50,17 +72,18 @@ fun CaloryHistoryList(
|
|||
// Bersihkan state saat komponen dihancurkan
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
viewModel.resetState()
|
||||
// Bersihkan gambar yang tidak digunakan saat meninggalkan layar
|
||||
viewModel.cleanupUnusedImages(context, username)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
if (viewModel.isLoading) {
|
||||
if (viewModel.isLoading.value) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
color = Color(0xFF4AB54A) // Hijau sesuai dengan tema
|
||||
color = primary
|
||||
)
|
||||
} else if (viewModel.historyList.isEmpty()) {
|
||||
} else if (viewModel.calorieList.value.isEmpty()) {
|
||||
// Pesan jika list kosong
|
||||
Text(
|
||||
text = "Belum ada riwayat makanan",
|
||||
|
@ -78,10 +101,12 @@ fun CaloryHistoryList(
|
|||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(viewModel.historyList) { history ->
|
||||
items(viewModel.calorieList.value) { calory ->
|
||||
CaloryHistoryItem(
|
||||
history = history,
|
||||
onClick = { onItemClick(history) }
|
||||
calory = calory,
|
||||
viewModel = viewModel,
|
||||
onClick = { onItemClick(calory) },
|
||||
onDelete = { onItemDelete(calory) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
@ -94,7 +119,7 @@ fun CaloryHistoryList(
|
|||
}
|
||||
|
||||
// Error message
|
||||
viewModel.errorMessage?.let { error ->
|
||||
viewModel.error.value?.let { error ->
|
||||
Text(
|
||||
text = error,
|
||||
color = Color.Red,
|
||||
|
@ -107,13 +132,27 @@ fun CaloryHistoryList(
|
|||
}
|
||||
|
||||
/**
|
||||
* Item riwayat kalori
|
||||
* Item riwayat kalori dengan gambar
|
||||
*/
|
||||
@Composable
|
||||
fun CaloryHistoryItem(
|
||||
history: CaloryHistoryModel,
|
||||
onClick: () -> Unit
|
||||
calory: CaloryModel,
|
||||
viewModel: CaloryHistoryViewModel,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// State untuk menyimpan bitmap dari penyimpanan lokal
|
||||
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
// Load bitmap saat komponen dibuat
|
||||
LaunchedEffect(calory.imagePath) {
|
||||
if (calory.imagePath.isNotEmpty()) {
|
||||
bitmap = viewModel.getImageBitmap(context, calory.imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -124,16 +163,30 @@ fun CaloryHistoryItem(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF4AB54A).copy(alpha = 0.6f)) // Warna hijau semi-transparan sesuai gambar
|
||||
.background(primary2)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Tampilkan gambar jika tersedia
|
||||
bitmap?.let { bmp ->
|
||||
Image(
|
||||
bitmap = bmp.asImageBitmap(),
|
||||
contentDescription = "Food Image",
|
||||
modifier = Modifier
|
||||
.size(70.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
// Detail kalori
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "${history.totalCalories} Kalori",
|
||||
text = "${calory.calories} Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = Color.White,
|
||||
|
@ -152,9 +205,9 @@ fun CaloryHistoryItem(
|
|||
)
|
||||
}
|
||||
|
||||
// Timestamp (opsional)
|
||||
// Tanggal
|
||||
Text(
|
||||
text = history.getFormattedDate(),
|
||||
text = formatDate(calory.date),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
|
@ -165,3 +218,121 @@ fun CaloryHistoryItem(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format tanggal untuk tampilan
|
||||
*/
|
||||
fun formatDate(dateString: String): String {
|
||||
return try {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val outputFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||
val date = inputFormat.parse(dateString)
|
||||
outputFormat.format(date ?: Date())
|
||||
} catch (e: Exception) {
|
||||
dateString // Kembalikan string asli jika format tidak sesuai
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Item riwayat kalori dengan opsi hapus
|
||||
*/
|
||||
@Composable
|
||||
fun CaloryHistoryItemWithDelete(
|
||||
calory: CaloryModel,
|
||||
viewModel: CaloryHistoryViewModel,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// State untuk menyimpan bitmap dari penyimpanan lokal
|
||||
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
// Load bitmap saat komponen dibuat
|
||||
LaunchedEffect(calory.imagePath) {
|
||||
if (calory.imagePath.isNotEmpty()) {
|
||||
bitmap = viewModel.getImageBitmap(context, calory.imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(primary2)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Tampilkan gambar jika tersedia
|
||||
bitmap?.let { bmp ->
|
||||
Image(
|
||||
bitmap = bmp.asImageBitmap(),
|
||||
contentDescription = "Food Image",
|
||||
modifier = Modifier
|
||||
.size(70.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
// Detail kalori
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "${calory.calories} Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = Color.White,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Lihat Detail",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = Color.White,
|
||||
fontFamily = medium,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Tanggal
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = formatDate(calory.date),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Tombol hapus
|
||||
Text(
|
||||
text = "Hapus",
|
||||
modifier = Modifier.clickable(onClick = onDelete),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = Color.White,
|
||||
fontFamily = medium,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
package com.example.caloryapp.widget
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.caloryapp.model.CaloryModel
|
||||
import com.example.caloryapp.ui.theme.bold
|
||||
import com.example.caloryapp.ui.theme.medium
|
||||
import com.example.caloryapp.ui.theme.primary
|
||||
import com.example.caloryapp.ui.theme.semibold
|
||||
import com.example.caloryapp.utils.CaloryCalculator
|
||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun CaloryStatisticsCard(
|
||||
userViewModel: UserViewModel,
|
||||
caloryHistoryViewModel: CaloryHistoryViewModel
|
||||
) {
|
||||
val user = userViewModel.user.value ?: return
|
||||
val caloriesList = caloryHistoryViewModel.calorieList.value
|
||||
|
||||
// Hitung statistik
|
||||
val weeklyTotal = calculateWeeklyTotal(caloriesList)
|
||||
val monthlyTotal = calculateMonthlyTotal(caloriesList)
|
||||
|
||||
// Kebutuhan kalori mingguan dan bulanan
|
||||
val dailyNeeds = CaloryCalculator.getDailyCaloryNeeds(user.gender)
|
||||
val weeklyNeeds = dailyNeeds * 7
|
||||
val monthlyNeeds = dailyNeeds * 30
|
||||
|
||||
// Persentase konsumsi
|
||||
val weeklyPercentage = (weeklyTotal.toFloat() / weeklyNeeds) * 100
|
||||
val monthlyPercentage = (monthlyTotal.toFloat() / monthlyNeeds) * 100
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = 4.dp,
|
||||
backgroundColor = Color.White
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Statistik Konsumsi Kalori",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.Black,
|
||||
fontFamily = bold
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Statistik mingguan
|
||||
Text(
|
||||
text = "Minggu Ini",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
color = Color.Gray,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = (weeklyPercentage / 100).coerceIn(0f, 1f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp),
|
||||
backgroundColor = Color.LightGray,
|
||||
color = primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "$weeklyTotal kkal",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = primary,
|
||||
fontFamily = medium
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "$weeklyNeeds kkal",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = Color.Gray,
|
||||
fontFamily = medium
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Statistik bulanan
|
||||
Text(
|
||||
text = "Bulan Ini",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
color = Color.Gray,
|
||||
fontFamily = semibold
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = (monthlyPercentage / 100).coerceIn(0f, 1f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp),
|
||||
backgroundColor = Color.LightGray,
|
||||
color = primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "$monthlyTotal kkal",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = primary,
|
||||
fontFamily = medium
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "$monthlyNeeds kkal",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = Color.Gray,
|
||||
fontFamily = medium
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk menghitung total kalori mingguan
|
||||
fun calculateWeeklyTotal(caloriesList: List<CaloryModel>): Int {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.set(Calendar.DAY_OF_WEEK, calendar.firstDayOfWeek)
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||
calendar.set(Calendar.MINUTE, 0)
|
||||
calendar.set(Calendar.SECOND, 0)
|
||||
calendar.set(Calendar.MILLISECOND, 0)
|
||||
|
||||
val startOfWeek = calendar.timeInMillis
|
||||
|
||||
calendar.add(Calendar.DAY_OF_WEEK, 6)
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 23)
|
||||
calendar.set(Calendar.MINUTE, 59)
|
||||
calendar.set(Calendar.SECOND, 59)
|
||||
|
||||
val endOfWeek = calendar.timeInMillis
|
||||
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
return caloriesList
|
||||
.filter {
|
||||
try {
|
||||
val date = dateFormat.parse(it.date)
|
||||
date != null && date.time >= startOfWeek && date.time <= endOfWeek
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
.sumOf { it.calories }
|
||||
}
|
||||
|
||||
// Fungsi untuk menghitung total kalori bulanan
|
||||
fun calculateMonthlyTotal(caloriesList: List<CaloryModel>): Int {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.set(Calendar.DAY_OF_MONTH, 1)
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||
calendar.set(Calendar.MINUTE, 0)
|
||||
calendar.set(Calendar.SECOND, 0)
|
||||
calendar.set(Calendar.MILLISECOND, 0)
|
||||
|
||||
val startOfMonth = calendar.timeInMillis
|
||||
|
||||
calendar.add(Calendar.MONTH, 1)
|
||||
calendar.add(Calendar.DAY_OF_MONTH, -1)
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 23)
|
||||
calendar.set(Calendar.MINUTE, 59)
|
||||
calendar.set(Calendar.SECOND, 59)
|
||||
|
||||
val endOfMonth = calendar.timeInMillis
|
||||
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
return caloriesList
|
||||
.filter {
|
||||
try {
|
||||
val date = dateFormat.parse(it.date)
|
||||
date != null && date.time >= startOfMonth && date.time <= endOfMonth
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
.sumOf { it.calories }
|
||||
}
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
//noinspection UsingMaterialAndMaterial3Libraries
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
//noinspection UsingMaterialAndMaterial3Libraries
|
||||
|
@ -14,6 +15,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.caloryapp.R
|
||||
|
@ -27,7 +29,8 @@ fun CustomTextField(
|
|||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
placeholderText: String,
|
||||
input: Boolean
|
||||
input: Boolean,
|
||||
keyboardOptions: KeyboardOptions
|
||||
) {
|
||||
OutlinedTextField(
|
||||
textStyle = TextStyle(
|
||||
|
@ -36,9 +39,11 @@ fun CustomTextField(
|
|||
fontFamily = semibold,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
|
||||
value = value,
|
||||
enabled = input,
|
||||
onValueChange = onValueChange,
|
||||
keyboardOptions = keyboardOptions,
|
||||
placeholder = {
|
||||
androidx.compose.material.Text(
|
||||
text = placeholderText,
|
||||
|
|
Loading…
Reference in New Issue