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,