diff --git a/.idea/other.xml b/.idea/other.xml index 05e8821..3f837c1 100644 --- a/.idea/other.xml +++ b/.idea/other.xml @@ -3,6 +3,17 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 18a05b1..1c5cf9b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/assets/model_food_calories2.h5 b/app/src/main/assets/model_food_calories2.h5 new file mode 100644 index 0000000..ae1820e Binary files /dev/null and b/app/src/main/assets/model_food_calories2.h5 differ diff --git a/app/src/main/assets/model_food_calories2.tflite b/app/src/main/assets/model_food_calories2.tflite new file mode 100644 index 0000000..2d69a3d Binary files /dev/null and b/app/src/main/assets/model_food_calories2.tflite differ diff --git a/app/src/main/assets/model_food_plate_densenet2.h5 b/app/src/main/assets/model_food_plate_densenet2.h5 new file mode 100644 index 0000000..b369068 Binary files /dev/null and b/app/src/main/assets/model_food_plate_densenet2.h5 differ diff --git a/app/src/main/assets/model_food_plate_densenet2.tflite b/app/src/main/assets/model_food_plate_densenet2.tflite new file mode 100644 index 0000000..ad6429a Binary files /dev/null and b/app/src/main/assets/model_food_plate_densenet2.tflite differ diff --git a/app/src/main/assets/model_food_plate_densenet5.h5 b/app/src/main/assets/model_food_plate_densenet5.h5 new file mode 100644 index 0000000..2855f73 Binary files /dev/null and b/app/src/main/assets/model_food_plate_densenet5.h5 differ diff --git a/app/src/main/assets/model_food_plate_densenet5.tflite b/app/src/main/assets/model_food_plate_densenet5.tflite new file mode 100644 index 0000000..23e1a89 Binary files /dev/null and b/app/src/main/assets/model_food_plate_densenet5.tflite differ diff --git a/app/src/main/java/com/example/caloryapp/MainActivity.kt b/app/src/main/java/com/example/caloryapp/MainActivity.kt index 3aed878..e5d8ec2 100644 --- a/app/src/main/java/com/example/caloryapp/MainActivity.kt +++ b/app/src/main/java/com/example/caloryapp/MainActivity.kt @@ -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() -// 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) } } } diff --git a/app/src/main/java/com/example/caloryapp/foodmodel/FoodCategory.kt b/app/src/main/java/com/example/caloryapp/foodmodel/FoodCategory.kt index 626b611..28e18d1 100644 --- a/app/src/main/java/com/example/caloryapp/foodmodel/FoodCategory.kt +++ b/app/src/main/java/com/example/caloryapp/foodmodel/FoodCategory.kt @@ -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.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() } } \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/foodmodel/FoodDetector.kt b/app/src/main/java/com/example/caloryapp/foodmodel/FoodDetector.kt index 52a72f0..5ec1d5a 100644 --- a/app/src/main/java/com/example/caloryapp/foodmodel/FoodDetector.kt +++ b/app/src/main/java/com/example/caloryapp/foodmodel/FoodDetector.kt @@ -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 diff --git a/app/src/main/java/com/example/caloryapp/model/CaloryModel.kt b/app/src/main/java/com/example/caloryapp/model/CaloryModel.kt new file mode 100644 index 0000000..ed7aa76 --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/model/CaloryModel.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/navigation/Navigation.kt b/app/src/main/java/com/example/caloryapp/navigation/Navigation.kt index 880be42..1d3f3b5 100644 --- a/app/src/main/java/com/example/caloryapp/navigation/Navigation.kt +++ b/app/src/main/java/com/example/caloryapp/navigation/Navigation.kt @@ -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) // } diff --git a/app/src/main/java/com/example/caloryapp/navigation/NavigationScreen.kt b/app/src/main/java/com/example/caloryapp/navigation/NavigationScreen.kt index 794ee04..84355ee 100644 --- a/app/src/main/java/com/example/caloryapp/navigation/NavigationScreen.kt +++ b/app/src/main/java/com/example/caloryapp/navigation/NavigationScreen.kt @@ -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") diff --git a/app/src/main/java/com/example/caloryapp/pages/MainScreen.kt b/app/src/main/java/com/example/caloryapp/pages/MainScreen.kt index 6c88e33..f13de39 100644 --- a/app/src/main/java/com/example/caloryapp/pages/MainScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/MainScreen.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/pages/account/ProfileDetailScreen.kt b/app/src/main/java/com/example/caloryapp/pages/account/ProfileDetailScreen.kt index 7967382..7a5ae09 100644 --- a/app/src/main/java/com/example/caloryapp/pages/account/ProfileDetailScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/account/ProfileDetailScreen.kt @@ -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 diff --git a/app/src/main/java/com/example/caloryapp/pages/account/ProfileScreen.kt b/app/src/main/java/com/example/caloryapp/pages/account/ProfileScreen.kt index f20fbda..c284512 100644 --- a/app/src/main/java/com/example/caloryapp/pages/account/ProfileScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/account/ProfileScreen.kt @@ -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() } diff --git a/app/src/main/java/com/example/caloryapp/pages/calorydetail/CaloryDetailScreen.kt b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CaloryDetailScreen.kt index 4e6dc6c..fd75c2d 100644 --- a/app/src/main/java/com/example/caloryapp/pages/calorydetail/CaloryDetailScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CaloryDetailScreen.kt @@ -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(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 ke Map - 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassificationScreen.kt b/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassificationScreen.kt index 92822de..3c34da1 100644 --- a/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassificationScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassificationScreen.kt @@ -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(null) } +// var saveMessage by remember { mutableStateOf(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(null) } + var capturedBitmap by remember { mutableStateOf(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 +// ) +// ) +// } +// } } } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/example/caloryapp/pages/dashboard/HomeScreen.kt b/app/src/main/java/com/example/caloryapp/pages/dashboard/HomeScreen.kt index 86c4093..08ab538 100644 --- a/app/src/main/java/com/example/caloryapp/pages/dashboard/HomeScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/dashboard/HomeScreen.kt @@ -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()) } + + // 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)) - } } } } diff --git a/app/src/main/java/com/example/caloryapp/pages/onboard/ChangePasswordScreen.kt b/app/src/main/java/com/example/caloryapp/pages/onboard/ChangePasswordScreen.kt index 6b879ce..ed03151 100644 --- a/app/src/main/java/com/example/caloryapp/pages/onboard/ChangePasswordScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/onboard/ChangePasswordScreen.kt @@ -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( diff --git a/app/src/main/java/com/example/caloryapp/pages/onboard/ForgotPasswordScreen.kt b/app/src/main/java/com/example/caloryapp/pages/onboard/ForgotPasswordScreen.kt index 9323278..6e72aed 100644 --- a/app/src/main/java/com/example/caloryapp/pages/onboard/ForgotPasswordScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/onboard/ForgotPasswordScreen.kt @@ -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( diff --git a/app/src/main/java/com/example/caloryapp/pages/onboard/LoginScreen.kt b/app/src/main/java/com/example/caloryapp/pages/onboard/LoginScreen.kt index 731178a..cf24e8e 100644 --- a/app/src/main/java/com/example/caloryapp/pages/onboard/LoginScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/onboard/LoginScreen.kt @@ -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( diff --git a/app/src/main/java/com/example/caloryapp/pages/onboard/RegisterScreen.kt b/app/src/main/java/com/example/caloryapp/pages/onboard/RegisterScreen.kt index 1d6d81a..b7bb0fb 100644 --- a/app/src/main/java/com/example/caloryapp/pages/onboard/RegisterScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/onboard/RegisterScreen.kt @@ -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 diff --git a/app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt b/app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt index d4c6d59..102a2bf 100644 --- a/app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt +++ b/app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt @@ -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) -> 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() + 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 } diff --git a/app/src/main/java/com/example/caloryapp/repository/UserRepository.kt b/app/src/main/java/com/example/caloryapp/repository/UserRepository.kt index 722ab69..2a038a0 100644 --- a/app/src/main/java/com/example/caloryapp/repository/UserRepository.kt +++ b/app/src/main/java/com/example/caloryapp/repository/UserRepository.kt @@ -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 diff --git a/app/src/main/java/com/example/caloryapp/ui/theme/Color.kt b/app/src/main/java/com/example/caloryapp/ui/theme/Color.kt index a8e182f..ee1daff 100644 --- a/app/src/main/java/com/example/caloryapp/ui/theme/Color.kt +++ b/app/src/main/java/com/example/caloryapp/ui/theme/Color.kt @@ -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) diff --git a/app/src/main/java/com/example/caloryapp/utils/CaloryCalculator.kt b/app/src/main/java/com/example/caloryapp/utils/CaloryCalculator.kt new file mode 100644 index 0000000..edf7e26 --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/utils/CaloryCalculator.kt @@ -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): 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/utils/LocalImageStorage.kt b/app/src/main/java/com/example/caloryapp/utils/LocalImageStorage.kt new file mode 100644 index 0000000..9b9b64e --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/utils/LocalImageStorage.kt @@ -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) { + 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}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/utils/SessionManager.kt b/app/src/main/java/com/example/caloryapp/utils/SessionManager.kt new file mode 100644 index 0000000..85e205d --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/utils/SessionManager.kt @@ -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}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/viewmodel/CaloryHistoryViewModel.kt b/app/src/main/java/com/example/caloryapp/viewmodel/CaloryHistoryViewModel.kt index fde83d7..d5df573 100644 --- a/app/src/main/java/com/example/caloryapp/viewmodel/CaloryHistoryViewModel.kt +++ b/app/src/main/java/com/example/caloryapp/viewmodel/CaloryHistoryViewModel.kt @@ -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>(emptyList()) + val calorieList: State> = _calorieList - // State untuk UI - var isLoading by mutableStateOf(false) - var errorMessage by mutableStateOf(null) + private val _isLoading = mutableStateOf(false) + val isLoading: State = _isLoading - // List riwayat kalori - var historyList by mutableStateOf>(emptyList()) + private val _error = mutableStateOf(null) + val error: State = _error - // Detail riwayat kalori yang dipilih - var selectedHistory by mutableStateOf(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 + // Mendapatkan bitmap dari path gambar + fun getImageBitmap(context: Context, imagePath: String): Bitmap? { + if (imagePath.isEmpty()) return null + return LocalImageStorage.getImageFromInternalStorage(context, imagePath) + } - 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) - } - } + // Membersihkan cache gambar yang tidak digunakan + fun cleanupUnusedImages(context: Context, username: String) { + viewModelScope.launch(Dispatchers.IO) { + repository.cleanupUnusedImages(context, username) } } +} - /** - * Reset state - */ - fun resetState() { - historyList = emptyList() - selectedHistory = null - errorMessage = null - isLoading = false - } -} \ No newline at end of file +//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(null) +// +// // List riwayat kalori +// var historyList by mutableStateOf>(emptyList()) +// +// // Detail riwayat kalori yang dipilih +// var selectedHistory by mutableStateOf(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 +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/viewmodel/CaloryViewModel.kt b/app/src/main/java/com/example/caloryapp/viewmodel/CaloryViewModel.kt new file mode 100644 index 0000000..a339682 --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/viewmodel/CaloryViewModel.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/com/example/caloryapp/viewmodel/FoodDetectionViewModel.kt b/app/src/main/java/com/example/caloryapp/viewmodel/FoodDetectionViewModel.kt index 0bfaa1c..500596a 100644 --- a/app/src/main/java/com/example/caloryapp/viewmodel/FoodDetectionViewModel.kt +++ b/app/src/main/java/com/example/caloryapp/viewmodel/FoodDetectionViewModel.kt @@ -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(null) private set + var isSaving by mutableStateOf(false) + private set + + var saveSuccess by mutableStateOf(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") } -} \ No newline at end of file + + // 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(null) +// private set +// +// var isLoading by mutableStateOf(false) +// private set +// +// var errorMessage by mutableStateOf(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) +// } +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/viewmodel/UserViewModel.kt b/app/src/main/java/com/example/caloryapp/viewmodel/UserViewModel.kt index 1d32528..20abbb0 100644 --- a/app/src/main/java/com/example/caloryapp/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/example/caloryapp/viewmodel/UserViewModel.kt @@ -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(null) val registerState: StateFlow = _registerState // Menyimpan data login state (loading, success, error) - private val _loginstate = mutableStateOf(LoginState.Loading) - val loginstate = _loginstate + private val _loginState = mutableStateOf(LoginState.Loading) + val loginState: State = _loginState // Menyimpan data pengguna yang login private val _user = mutableStateOf(null) - val user = _user + val user: State = _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() { // } // } // } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/widget/CaloryHistoryList.kt b/app/src/main/java/com/example/caloryapp/widget/CaloryHistoryList.kt index 73043b4..b88cbfb 100644 --- a/app/src/main/java/com/example/caloryapp/widget/CaloryHistoryList.kt +++ b/app/src/main/java/com/example/caloryapp/widget/CaloryHistoryList.kt @@ -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(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), @@ -164,4 +217,122 @@ 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(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 + ) + ) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/widget/CaloryStatisticsCard.kt b/app/src/main/java/com/example/caloryapp/widget/CaloryStatisticsCard.kt new file mode 100644 index 0000000..080aecf --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/widget/CaloryStatisticsCard.kt @@ -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): 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): 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 } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/widget/CustomTextField.kt b/app/src/main/java/com/example/caloryapp/widget/CustomTextField.kt index 19e4f5c..2745313 100644 --- a/app/src/main/java/com/example/caloryapp/widget/CustomTextField.kt +++ b/app/src/main/java/com/example/caloryapp/widget/CustomTextField.kt @@ -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,