This commit is contained in:
E41212133_Naufal Kadhafi 2025-07-01 14:33:06 +07:00
parent 7cd246cea6
commit 97ef889d46
37 changed files with 2947 additions and 531 deletions

View File

@ -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>

View File

@ -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.

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
// }

View File

@ -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")

View File

@ -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
)
}
}

View File

@ -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

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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
// )
// )
// }
// }
}
}
}
}

View File

@ -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))
}
}
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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}")
}
}
}

View File

@ -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}")
}
}
}

View File

@ -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
// }
//}

View File

@ -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)
}
}
}
}

View File

@ -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)
// }
// }
// }
//}

View File

@ -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() {
// }
// }
// }
}

View File

@ -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
)
)
}
}
}
}
}

View File

@ -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 }
}

View File

@ -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,