From 7cd246cea62ba2fc203134780a035889e9178616 Mon Sep 17 00:00:00 2001 From: E41212133_Naufal Kadhafi <100177539+Jizzyyy@users.noreply.github.com> Date: Sun, 11 May 2025 19:45:51 +0700 Subject: [PATCH] Progress 4 --- .idea/misc.xml | 1 - app/build.gradle.kts | 1 + .../com/example/caloryapp/MainActivity.kt | 9 - .../foodmodel/FoodDetectionViewModel.kt | 60 -- .../caloryapp/model/CaloryHistoryModel.kt | 37 ++ .../caloryapp/navigation/Navigation.kt | 8 +- .../com/example/caloryapp/pages/MainScreen.kt | 10 +- .../pages/calorydetail/CaloryDetailScreen.kt | 269 ++++++++ .../CameraDetectionScreen.kt | 5 +- .../{camera => calorydetail}/CameraPreview.kt | 20 +- .../CameraPreviewWithOverlay.kt | 4 +- .../calorydetail/FoodClassificationScreen.kt | 573 ++++++++++++++++++ .../FoodClassifier.kt | 2 +- .../{camera => calorydetail}/FoodTest2.kt | 5 +- .../caloryapp/pages/camera/ScreenTest.kt | 259 -------- .../caloryapp/pages/dashboard/HomeScreen.kt | 96 +++ .../caloryapp/repository/CaloryRepository.kt | 129 ++++ .../viewmodel/CaloryHistoryViewModel.kt | 129 ++++ .../viewmodel/FoodDetectionViewModel.kt | 191 ++++++ .../caloryapp/widget/CaloryHistoryList.kt | 167 +++++ gradle/libs.versions.toml | 2 + 21 files changed, 1610 insertions(+), 367 deletions(-) delete mode 100644 app/src/main/java/com/example/caloryapp/foodmodel/FoodDetectionViewModel.kt create mode 100644 app/src/main/java/com/example/caloryapp/model/CaloryHistoryModel.kt create mode 100644 app/src/main/java/com/example/caloryapp/pages/calorydetail/CaloryDetailScreen.kt rename app/src/main/java/com/example/caloryapp/pages/{camera => calorydetail}/CameraDetectionScreen.kt (92%) rename app/src/main/java/com/example/caloryapp/pages/{camera => calorydetail}/CameraPreview.kt (76%) rename app/src/main/java/com/example/caloryapp/pages/{camera => calorydetail}/CameraPreviewWithOverlay.kt (93%) create mode 100644 app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassificationScreen.kt rename app/src/main/java/com/example/caloryapp/pages/{camera => calorydetail}/FoodClassifier.kt (93%) rename app/src/main/java/com/example/caloryapp/pages/{camera => calorydetail}/FoodTest2.kt (98%) delete mode 100644 app/src/main/java/com/example/caloryapp/pages/camera/ScreenTest.kt create mode 100644 app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt create mode 100644 app/src/main/java/com/example/caloryapp/viewmodel/CaloryHistoryViewModel.kt create mode 100644 app/src/main/java/com/example/caloryapp/viewmodel/FoodDetectionViewModel.kt create mode 100644 app/src/main/java/com/example/caloryapp/widget/CaloryHistoryList.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index e485240..2188b52 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1e0b27a..18a05b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(libs.androidx.credentials) implementation(libs.androidx.credentials.play.services.auth) implementation(libs.googleid) + implementation(libs.firebase.storage) // implementation(libs.firebase.auth.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/java/com/example/caloryapp/MainActivity.kt b/app/src/main/java/com/example/caloryapp/MainActivity.kt index 8acf70a..3aed878 100644 --- a/app/src/main/java/com/example/caloryapp/MainActivity.kt +++ b/app/src/main/java/com/example/caloryapp/MainActivity.kt @@ -7,18 +7,9 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Surface -import androidx.compose.material3.MaterialTheme -import androidx.compose.ui.Modifier import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.caloryapp.foodmodel.FoodDetectionViewModel import com.example.caloryapp.navigation.Navigation -import com.example.caloryapp.pages.camera.FoodCalorieScreen -import com.example.caloryapp.pages.camera.FoodCalorieViewModel2 -import com.example.caloryapp.pages.camera.ScreenTest //import com.example.caloryapp.pages.NavBarScreen import com.example.caloryapp.ui.theme.CaloryAppTheme diff --git a/app/src/main/java/com/example/caloryapp/foodmodel/FoodDetectionViewModel.kt b/app/src/main/java/com/example/caloryapp/foodmodel/FoodDetectionViewModel.kt deleted file mode 100644 index 71a35a5..0000000 --- a/app/src/main/java/com/example/caloryapp/foodmodel/FoodDetectionViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.caloryapp.foodmodel - -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 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") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/model/CaloryHistoryModel.kt b/app/src/main/java/com/example/caloryapp/model/CaloryHistoryModel.kt new file mode 100644 index 0000000..89fcb0b --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/model/CaloryHistoryModel.kt @@ -0,0 +1,37 @@ +package com.example.caloryapp.model + +import com.example.caloryapp.foodmodel.FoodCategory +import com.google.firebase.Timestamp +import java.util.Date + +/** + * Model untuk menyimpan riwayat kalori makanan + */ +data class CaloryHistoryModel( + val id: String = "", // ID dokumen + val username: String = "", // Username pengguna + val timestamp: Timestamp = Timestamp.now(), // Waktu pencatatan + val totalCalories: Int = 0, // Total kalori + // Daftar persentase kategori makanan yang terdeteksi + val foodComposition: Map = mapOf(), + // Daftar kalori per kategori yang dihitung + val caloriesPerCategory: Map = mapOf() +) { + // Constructor kosong untuk Firestore + constructor() : this("", "", Timestamp.now(), 0, mapOf(), mapOf()) + + // Fungsi untuk mengkonversi timestamp ke Date + fun getDate(): Date { + return timestamp.toDate() + } + + // Fungsi untuk mendapatkan tanggal dalam format string + fun getFormattedDate(): String { + val date = timestamp.toDate() + val day = date.date + val month = date.month + 1 // Months are 0-based + val year = date.year + 1900 // Years start from 1900 + + return "$day/$month/$year" + } +} \ 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 d270b0d..880be42 100644 --- a/app/src/main/java/com/example/caloryapp/navigation/Navigation.kt +++ b/app/src/main/java/com/example/caloryapp/navigation/Navigation.kt @@ -9,12 +9,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.example.caloryapp.foodmodel.FoodDetectionViewModel +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.camera.ScreenTest +import com.example.caloryapp.pages.calorydetail.ScreenTest import com.example.caloryapp.pages.onboard.ChangePasswordScreen import com.example.caloryapp.pages.onboard.ForgotPasswordScreen import com.example.caloryapp.pages.onboard.LoginScreen @@ -23,6 +23,7 @@ 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.viewmodel.CaloryHistoryViewModel import com.example.caloryapp.viewmodel.UserViewModel @Composable @@ -30,6 +31,7 @@ fun Navigation(modifier: Modifier = Modifier) { val navController = rememberNavController() val userViewModel: UserViewModel = viewModel() val foodViewModel: FoodDetectionViewModel = viewModel() + val caloryHistoryViewModel: CaloryHistoryViewModel = viewModel() val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() @@ -70,7 +72,7 @@ fun Navigation(modifier: Modifier = Modifier) { // HomeScreen(navController = navController, userViewModel) // } composable(NavigationScreen.MainScreen.name) { - MainScreen(userViewModel, foodViewModel) + MainScreen(userViewModel, foodViewModel, caloryHistoryViewModel) } composable(NavigationScreen.ForgotPasswordScreen.name) { ForgotPasswordScreen(navController = navController) 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 4356821..6c88e33 100644 --- a/app/src/main/java/com/example/caloryapp/pages/MainScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/MainScreen.kt @@ -35,18 +35,19 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.example.caloryapp.R -import com.example.caloryapp.foodmodel.FoodDetectionViewModel +import com.example.caloryapp.viewmodel.FoodDetectionViewModel import com.example.caloryapp.navigation.NavigationScreen 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.camera.ScreenTest +import com.example.caloryapp.pages.calorydetail.ScreenTest import com.example.caloryapp.pages.dashboard.HomeScreen import com.example.caloryapp.pages.onboard.LoginScreen 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.viewmodel.CaloryHistoryViewModel import com.example.caloryapp.viewmodel.UserViewModel import kotlinx.coroutines.launch @@ -59,7 +60,8 @@ sealed class DrawerScreen(val title: String) { @Composable fun MainScreen( userViewModel: UserViewModel, - foodDetectionViewModel: FoodDetectionViewModel + foodDetectionViewModel: FoodDetectionViewModel, + caloryHistoryViewModel: CaloryHistoryViewModel ) { val navController = rememberNavController() val drawerState = rememberDrawerState(DrawerValue.Closed) @@ -73,7 +75,7 @@ fun MainScreen( ) { NavHost(navController = navController, startDestination = DrawerScreen.HomeScreen.title) { composable(DrawerScreen.HomeScreen.title) { - HomeScreen(navController = navController, drawerState = drawerState, scope = scope, userViewModel) + HomeScreen(navController = navController, drawerState = drawerState, scope = scope, caloryHistoryViewModel, userViewModel) } composable(DrawerScreen.ProfileScreen.title) { ProfileScreen(navController = navController, drawerState = drawerState, scope = scope, viewModel = userViewModel) 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 new file mode 100644 index 0000000..4e6dc6c --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CaloryDetailScreen.kt @@ -0,0 +1,269 @@ +package com.example.caloryapp.pages.calorydetail + +import androidx.compose.foundation.background +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.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.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.ui.theme.background +import com.example.caloryapp.ui.theme.bold +import com.example.caloryapp.ui.theme.primary +import com.example.caloryapp.ui.theme.semibold +import com.example.caloryapp.viewmodel.CaloryHistoryViewModel +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Locale + +/** + * Layar detail riwayat kalori + */ +@Composable +fun CaloryHistoryDetailScreen( + navController: NavController, + historyId: String, + viewModel: CaloryHistoryViewModel +) { + val coroutineScope = rememberCoroutineScope() + + // Muat data detail saat komponen ditampilkan + LaunchedEffect(historyId) { + viewModel.loadHistoryDetail(historyId) + } + + // Bersihkan state detail saat komponen dihancurkan + DisposableEffect(Unit) { + onDispose { + viewModel.selectedHistory = null + } + } + + 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", + style = TextStyle( + fontSize = 24.sp, + color = primary, + fontFamily = bold + ), + modifier = Modifier.weight(1f) + ) + + // Tombol hapus + IconButton( + onClick = { + coroutineScope.launch { + viewModel.deleteHistory(historyId) { success -> + if (success) { + navController.popBackStack() + } + } + } + } + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Hapus", + tint = Color.Red + ) + } + } + + Spacer(modifier = Modifier.height(30.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) + ) + } + + Spacer(modifier = Modifier.height(32.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( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "$icon $categoryName", + color = Color(android.graphics.Color.parseColor(colorHex)) + ) + + 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) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/pages/camera/CameraDetectionScreen.kt b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraDetectionScreen.kt similarity index 92% rename from app/src/main/java/com/example/caloryapp/pages/camera/CameraDetectionScreen.kt rename to app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraDetectionScreen.kt index 3c2478e..6004a37 100644 --- a/app/src/main/java/com/example/caloryapp/pages/camera/CameraDetectionScreen.kt +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraDetectionScreen.kt @@ -1,15 +1,12 @@ -package com.example.caloryapp.pages.camera +package com.example.caloryapp.pages.calorydetail -import android.widget.Toast import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavController @Composable diff --git a/app/src/main/java/com/example/caloryapp/pages/camera/CameraPreview.kt b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraPreview.kt similarity index 76% rename from app/src/main/java/com/example/caloryapp/pages/camera/CameraPreview.kt rename to app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraPreview.kt index ac00d41..eb3e865 100644 --- a/app/src/main/java/com/example/caloryapp/pages/camera/CameraPreview.kt +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraPreview.kt @@ -1,27 +1,9 @@ -package com.example.caloryapp.pages.camera +package com.example.caloryapp.pages.calorydetail -import android.Manifest -import android.content.Context -import android.util.Size -import android.view.Surface -import android.view.ViewGroup import androidx.annotation.OptIn -import androidx.camera.core.CameraSelector import androidx.camera.core.ExperimentalGetImage -import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy -import androidx.camera.core.Preview -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.view.PreviewView -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat -import androidx.lifecycle.LifecycleOwner import java.nio.ByteBuffer -import java.util.concurrent.Executors //@Composable diff --git a/app/src/main/java/com/example/caloryapp/pages/camera/CameraPreviewWithOverlay.kt b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraPreviewWithOverlay.kt similarity index 93% rename from app/src/main/java/com/example/caloryapp/pages/camera/CameraPreviewWithOverlay.kt rename to app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraPreviewWithOverlay.kt index e386dff..b8bda4f 100644 --- a/app/src/main/java/com/example/caloryapp/pages/camera/CameraPreviewWithOverlay.kt +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/CameraPreviewWithOverlay.kt @@ -1,10 +1,8 @@ -package com.example.caloryapp.pages.camera +package com.example.caloryapp.pages.calorydetail -import android.content.Context import android.widget.Toast import androidx.compose.foundation.layout.Box import androidx.compose.runtime.* -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @Composable 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 new file mode 100644 index 0000000..92822de --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassificationScreen.kt @@ -0,0 +1,573 @@ +package com.example.caloryapp.pages.calorydetail +// +//import android.net.Uri +//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.material.Button +//import androidx.compose.material.ButtonDefaults +//import androidx.compose.material.CircularProgressIndicator +//import androidx.compose.material.SnackbarHost +//import androidx.compose.material.SnackbarHostState +//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 +//import androidx.compose.runtime.rememberCoroutineScope +//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.font.FontWeight +//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.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 kotlinx.coroutines.launch +// +//@Composable +//fun ScreenTest( +// navController: NavController, +// viewModel: FoodDetectionViewModel, +// currentUsername: String? = null +//) { +// val context = LocalContext.current +// val coroutineScope = rememberCoroutineScope() +// val snackbarHostState = remember { SnackbarHostState() } +// var selectedImageUri by remember { mutableStateOf(null) } +// // state untuk menyimpan username +// var username by remember { mutableStateOf(currentUsername ?: "") } +// var showUsernameInput by remember { mutableStateOf(currentUsername == null) } +// +// // launcher untuk memilih gambar +// val getContent = rememberLauncherForActivityResult( +// contract = ActivityResultContracts.GetContent() +// ) { uri: Uri? -> +// uri?.let { +// selectedImageUri = it +// viewModel.detectFoodFromImage(context, it) +// } +// } +// +// LaunchedEffect(viewModel.saveSuccess) { +// viewModel.saveSuccess?.let { success -> +// if (success) { +// snackbarHostState.showSnackbar("Berhasil menyimpan data kalori!") +// } else if (success == false) { +// snackbarHostState.showSnackbar("Gagal menyimpan data: ${viewModel.errorMessage ?: "Unknown error"}") +// } +// } +// } +// +// // UI Layout +// Box(modifier = Modifier.fillMaxSize()) { +// 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 +// ) +// ) +// +// // Input username jika belum login +// if (showUsernameInput) { +// Spacer(modifier = Modifier.height(16.dp)) +// androidx.compose.material.TextField( +// value = username, +// onValueChange = { username = it }, +// label = { Text("Username") }, +// modifier = Modifier.fillMaxWidth() +// ) +// } +// +// // 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(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 +// ) { +// Text( +// text = "Pilih Gambar Makanan", +// style = TextStyle( +// fontSize = 18.sp, +// color = primary, +// letterSpacing = 1.sp, +// fontFamily = medium, +// textAlign = TextAlign.Center +// ) +// ) +// } +// } +// Spacer(modifier = Modifier.height(40.dp)) +// } +// +// // Button to select image +// Button( +// onClick = { +// getContent.launch("image/*") +// }, +// modifier = Modifier +// .width(360.dp) +// .height(50.dp), +// colors = ButtonDefaults.buttonColors(backgroundColor = primary), +// shape = RoundedCornerShape(20.dp) +// ) { +// Text( +// text = "Pilih", +// style = TextStyle( +// fontSize = 18.sp, +// color = Color.White, +// fontFamily = semibold, +// textAlign = TextAlign.Center +// ) +// ) +// } +// +// Spacer(modifier = Modifier.height(32.dp)) +// +// // Loading indicator +// if (viewModel.isLoading) { +// Row( +// modifier = Modifier.fillMaxWidth(), +// horizontalArrangement = Arrangement.Center +// ) { +// CircularProgressIndicator(color = primary) +// } +// Spacer(modifier = Modifier.height(16.dp)) +// } +// +// // Error message +// 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 +// ) { +// // Food plate diagram +// PlateDiagram( +// categories = result.allCategories, +// modifier = Modifier +// .size(200.dp) +// .padding(8.dp) +// ) +// +// Spacer(modifier = Modifier.height(16.dp)) +// +// // Total calories +// val totalCalories = result.allCategories.calculateTotalCalories() +// +// 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)) +// +// // Categories breakdown +// 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), +// fontWeight = FontWeight.Medium +// ) +// } +// } +// +// // Button to save results +// Spacer(modifier = Modifier.height(24.dp)) +// +// if (viewModel.isSaving) { +// CircularProgressIndicator(color = primary) +// } else { +// Button( +// onClick = { +// if (username.isNotBlank()) { +// viewModel.saveDetectionResult(username) +// } else { +// coroutineScope.launch { +// snackbarHostState.showSnackbar("Masukkan username terlebih dahulu") +// } +// } +// }, +// modifier = Modifier +// .width(360.dp) +// .height(50.dp), +// colors = ButtonDefaults.buttonColors(backgroundColor = primary), +// shape = RoundedCornerShape(20.dp), +// enabled = !viewModel.isSaving && username.isNotBlank() +// ) { +// Text( +// text = "Simpan Hasil Deteksi", +// style = TextStyle( +// fontSize = 18.sp, +// color = Color.White, +// fontFamily = semibold, +// textAlign = TextAlign.Center +// ) +// ) +// } +// } +// } +// } +// } +// +// // Snackbar untuk notifikasi +// SnackbarHost( +// hostState = snackbarHostState, +// modifier = Modifier +// .align(Alignment.BottomCenter) +// .padding(16.dp) +// ) +// } +//} + +//package com.example.caloryapp.pages.camera + +import android.net.Uri +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.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 + +@Composable +fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel) { + val context = LocalContext.current + + var selectedImageUri by remember { mutableStateOf(null) } + + val getContent = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri: Uri? -> + uri?.let { + selectedImageUri = it + viewModel.detectFoodFromImage(context, it) + } + } + + 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 = { + 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 = "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() + } + + // Error message + 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)) + + // Main category and caloriesd + 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 + ) + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/caloryapp/pages/camera/FoodClassifier.kt b/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassifier.kt similarity index 93% rename from app/src/main/java/com/example/caloryapp/pages/camera/FoodClassifier.kt rename to app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassifier.kt index 1249a17..c8adff5 100644 --- a/app/src/main/java/com/example/caloryapp/pages/camera/FoodClassifier.kt +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodClassifier.kt @@ -1,4 +1,4 @@ -package com.example.caloryapp.pages.camera +package com.example.caloryapp.pages.calorydetail import android.content.Context import org.tensorflow.lite.Interpreter diff --git a/app/src/main/java/com/example/caloryapp/pages/camera/FoodTest2.kt b/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodTest2.kt similarity index 98% rename from app/src/main/java/com/example/caloryapp/pages/camera/FoodTest2.kt rename to app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodTest2.kt index 1c8e83d..627eb6c 100644 --- a/app/src/main/java/com/example/caloryapp/pages/camera/FoodTest2.kt +++ b/app/src/main/java/com/example/caloryapp/pages/calorydetail/FoodTest2.kt @@ -1,4 +1,4 @@ -package com.example.caloryapp.pages.camera +package com.example.caloryapp.pages.calorydetail import android.content.Context import android.graphics.Bitmap @@ -15,15 +15,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.core.content.FileProvider import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.tensorflow.lite.Interpreter -import org.tensorflow.lite.support.common.FileUtil -import java.io.File import java.io.FileInputStream import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/app/src/main/java/com/example/caloryapp/pages/camera/ScreenTest.kt b/app/src/main/java/com/example/caloryapp/pages/camera/ScreenTest.kt deleted file mode 100644 index 4e61791..0000000 --- a/app/src/main/java/com/example/caloryapp/pages/camera/ScreenTest.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.example.caloryapp.pages.camera - -import android.net.Uri -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.font.FontWeight -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.foodmodel.FoodDetectionViewModel -import com.example.caloryapp.foodmodel.PlateDiagram -import com.example.caloryapp.foodmodel.calculateTotalCalories -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 - -@Composable -fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel) { - val context = LocalContext.current - - var selectedImageUri by remember { mutableStateOf(null) } - - val getContent = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri: Uri? -> - uri?.let { - selectedImageUri = it - viewModel.detectFoodFromImage(context, it) - } - } - - 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 = { - 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 = "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() - } - - // Error message - 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)) - - // Main category and caloriesd - 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)) - -// Spacer(modifier = Modifier.height(16.dp)) - - // Categories breakdown -// Text( -// text = "Komposisi Makanan:", -// fontWeight = FontWeight.Medium, -// modifier = Modifier.padding(bottom = 8.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), - fontWeight = FontWeight.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 28ebb48..86c4093 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 @@ -14,11 +14,17 @@ 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 +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon 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 @@ -28,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -35,7 +42,10 @@ import com.example.caloryapp.R import com.example.caloryapp.navigation.NavigationScreen 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.semibold +import com.example.caloryapp.viewmodel.CaloryHistoryViewModel import com.example.caloryapp.viewmodel.UserViewModel import com.example.caloryapp.widget.FilterBar @@ -45,11 +55,19 @@ fun HomeScreen( navController: NavController, drawerState: DrawerState, scope: kotlinx.coroutines.CoroutineScope, + caloryHistoryViewModel: CaloryHistoryViewModel, viewModel: UserViewModel, ) { var selectedFilter by remember { mutableStateOf("Semua") } val user = viewModel.user.value + LaunchedEffect(user) { + user?.let { + // Memuat 4 riwayat terbaru untuk ditampilkan di home + caloryHistoryViewModel.loadHistoryByUsername(it.username, 2) + } + } + Box( modifier = Modifier .fillMaxSize() @@ -124,6 +142,84 @@ fun HomeScreen( // Filter Bar 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()) { + Text( + text = "Belum ada riwayat makanan", + style = TextStyle( + fontSize = 16.sp, + fontFamily = medium, + color = Color.Gray + ), + modifier = Modifier + .align(Alignment.Center) + .padding(vertical = 24.dp) + ) + } else { + // List riwayat kalori menggunakan LazyColumn khusus + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(caloryHistoryViewModel.historyList) { history -> + // Card item untuk riwayat kalori + 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 + ) + ) + + Text( + text = "Lihat Detail", + style = TextStyle( + fontSize = 14.sp, + color = Color.White, + fontFamily = medium, + textDecoration = TextDecoration.Underline + ) + ) + } + } + } + } + } + + // Spacer di akhir list + item { + Spacer(modifier = Modifier.height(70.dp)) + } + } + } + } } FloatingActionButton( modifier = Modifier diff --git a/app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt b/app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt new file mode 100644 index 0000000..d4c6d59 --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/repository/CaloryRepository.kt @@ -0,0 +1,129 @@ +package com.example.caloryapp.repository + +import android.util.Log +import com.example.caloryapp.foodmodel.FoodCategory +import com.example.caloryapp.foodmodel.FoodDetectionResult +import com.example.caloryapp.model.CaloryHistoryModel +import com.google.firebase.Timestamp +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query +import java.util.UUID + +class CaloryRepository { + private val db = FirebaseFirestore.getInstance() + private val TAG = "CaloryRepository" + + /** + * Menyimpan hasil deteksi makanan ke Firestore (tanpa gambar) + * @param username Username pengguna + * @param result Hasil deteksi makanan + * @param totalCalories Total kalori yang dihitung + * @param onComplete Callback setelah selesai + */ + fun saveCaloryHistory( + username: String, + result: FoodDetectionResult, + totalCalories: Int, + onComplete: (Boolean, String?) -> Unit + ) { + // Konversi map FoodCategory ke String untuk Firestore + val foodComposition = result.allCategories.mapKeys { it.key.displayName } + + // Hitung kalori per kategori + val caloriesPerCategory = result.allCategories.mapKeys { it.key.displayName } + .mapValues { (category, percentage) -> + // Asumsi berat porsi 500 gram + val weightGrams = 500 * percentage + val categoryEnum = FoodCategory.values().find { it.displayName == category } + val calories = categoryEnum?.let { + (weightGrams * it.caloriesPer100g / 100).toInt() + } ?: 0 + calories + } + + // Buat objek history + val docId = UUID.randomUUID().toString() + val caloryHistory = CaloryHistoryModel( + id = docId, + username = username, + timestamp = Timestamp.now(), + totalCalories = totalCalories, + foodComposition = foodComposition, + caloriesPerCategory = caloriesPerCategory + ) + + // Simpan ke Firestore + db.collection("calory_history") + .document(docId) + .set(caloryHistory) + .addOnSuccessListener { + Log.d(TAG, "Calory history saved successfully") + onComplete(true, docId) + } + .addOnFailureListener { e -> + Log.e(TAG, "Error saving calory history", e) + onComplete(false, null) + } + } + + /** + * Mengambil riwayat kalori berdasarkan username + */ + fun getCaloryHistoryByUsername( + username: String, + limit: Long = 10, + onComplete: (List) -> Unit + ) { + db.collection("calory_history") + .whereEqualTo("username", username) + .orderBy("timestamp", Query.Direction.DESCENDING) + .limit(limit) + .get() + .addOnSuccessListener { documents -> + val historyList = documents.mapNotNull { doc -> + doc.toObject(CaloryHistoryModel::class.java) + } + onComplete(historyList) + } + .addOnFailureListener { e -> + Log.e(TAG, "Error getting calory history", e) + onComplete(emptyList()) + } + } + + /** + * Mendapatkan detail riwayat kalori berdasarkan ID + */ + fun getCaloryHistoryById(id: String, onComplete: (CaloryHistoryModel?) -> Unit) { + db.collection("calory_history") + .document(id) + .get() + .addOnSuccessListener { document -> + if (document.exists()) { + val history = document.toObject(CaloryHistoryModel::class.java) + onComplete(history) + } else { + onComplete(null) + } + } + .addOnFailureListener { e -> + Log.e(TAG, "Error getting calory history detail", e) + onComplete(null) + } + } + + /** + * Menghapus riwayat kalori berdasarkan ID + */ + fun deleteCaloryHistory(id: String, onComplete: (Boolean) -> Unit) { + db.collection("calory_history") + .document(id) + .delete() + .addOnSuccessListener { + onComplete(true) + } + .addOnFailureListener { + onComplete(false) + } + } +} \ 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 new file mode 100644 index 0000000..fde83d7 --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/viewmodel/CaloryHistoryViewModel.kt @@ -0,0 +1,129 @@ +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/FoodDetectionViewModel.kt b/app/src/main/java/com/example/caloryapp/viewmodel/FoodDetectionViewModel.kt new file mode 100644 index 0000000..0bfaa1c --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/viewmodel/FoodDetectionViewModel.kt @@ -0,0 +1,191 @@ +//package com.example.caloryapp.viewmodel +// +//import android.content.Context +//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 com.example.caloryapp.foodmodel.FoodCategory +//import com.example.caloryapp.foodmodel.FoodDetectionResult +//import com.example.caloryapp.foodmodel.calculateTotalCalories +//import com.example.caloryapp.repository.CaloryRepository +//import kotlinx.coroutines.CoroutineScope +//import kotlinx.coroutines.Dispatchers +//import kotlinx.coroutines.launch +//import kotlinx.coroutines.withContext +// +//class FoodDetectionViewModel : ViewModel() { +// +// // Repository untuk menyimpan data kalori +// private val caloryRepository = CaloryRepository() +// +// // State untuk UI +// var isLoading by mutableStateOf(false) +// var detectionResult by mutableStateOf(null) +// var errorMessage by mutableStateOf(null) +// +// // State untuk tracking penyimpanan +// var isSaving by mutableStateOf(false) +// var saveSuccess by mutableStateOf(null) +// var lastSavedId by mutableStateOf(null) +// +// /** +// * Mendeteksi makanan dari gambar +// * Note: Implementasi sebenarnya akan menggunakan ML model +// */ +// fun detectFoodFromImage(context: Context, imageUri: Uri) { +// isLoading = true +// errorMessage = null +// +// // Simulasi proses deteksi (ganti dengan implementasi ML yang sebenarnya) +// // Dalam kasus nyata, ini akan menggunakan TensorFlow Lite atau ML Kit +// CoroutineScope(Dispatchers.IO).launch { +// try { +// // Lakukan deteksi (simulasi delay) +// withContext(Dispatchers.IO) { +// Thread.sleep(1500) // Simulasi delay proses ML +// } +// +// // Demo hasil - dalam aplikasi nyata ini akan datang dari model ML +// // Contoh hasil deteksi: makanan dengan karbohidrat dominan +// val detectedCategories = mapOf( +// FoodCategory.CARBS to 0.5f, // 50% karbohidrat +// FoodCategory.PROTEIN to 0.25f, // 25% protein +// FoodCategory.VEGETABLES to 0.15f, // 15% sayuran +// FoodCategory.FRUITS to 0.05f, // 5% buah +// FoodCategory.OTHER to 0.05f // 5% lainnya +// ) +// +// // Tentukan kategori utama (yang memiliki persentase tertinggi) +// val mainEntry = detectedCategories.maxByOrNull { it.value } +// ?: throw Exception("No categories detected") +// +// val result = FoodDetectionResult( +// mainCategory = mainEntry.key, +// confidence = mainEntry.value, +// allCategories = detectedCategories +// ) +// +// withContext(Dispatchers.Main) { +// detectionResult = result +// isLoading = false +// } +// } catch (e: Exception) { +// Log.e("FoodDetectionViewModel", "Error detecting food", e) +// withContext(Dispatchers.Main) { +// errorMessage = "Terjadi kesalahan: ${e.message}" +// isLoading = false +// } +// } +// } +// } +// +// /** +// * Menyimpan hasil deteksi makanan ke Firebase untuk pengguna yang sedang login +// */ +// fun saveDetectionResult(username: String) { +// // Validasi input +// val result = detectionResult ?: run { +// errorMessage = "Tidak ada hasil deteksi untuk disimpan" +// return +// } +// +// isSaving = true +// saveSuccess = null +// +// // Hitung total kalori +// val totalCalories = result.allCategories.calculateTotalCalories() +// +// // Simpan ke Firestore (tanpa gambar) +// caloryRepository.saveCaloryHistory( +// username = username, +// result = result, +// totalCalories = totalCalories +// ) { success, id -> +// isSaving = false +// saveSuccess = success +// lastSavedId = id +// +// if (!success) { +// errorMessage = "Gagal menyimpan data ke database" +// } +// } +// } +// +// /** +// * Reset state setelah navigasi atau selesai +// */ +// fun resetState() { +// detectionResult = null +// errorMessage = null +// isLoading = false +// isSaving = false +// saveSuccess = null +// lastSavedId = null +// } +//} + +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 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") + } +} \ 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 new file mode 100644 index 0000000..73043b4 --- /dev/null +++ b/app/src/main/java/com/example/caloryapp/widget/CaloryHistoryList.kt @@ -0,0 +1,167 @@ +package com.example.caloryapp.widget + + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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.ui.theme.medium +import com.example.caloryapp.ui.theme.semibold +import com.example.caloryapp.viewmodel.CaloryHistoryViewModel + +/** + * Komponen list riwayat kalori + */ +@Composable +fun CaloryHistoryList( + viewModel: CaloryHistoryViewModel, + username: String, + onItemClick: (CaloryHistoryModel) -> Unit = {} +) { + // Muat data saat komponen pertama kali ditampilkan + LaunchedEffect(username) { + viewModel.loadHistoryByUsername(username) + } + + // Bersihkan state saat komponen dihancurkan + DisposableEffect(Unit) { + onDispose { + viewModel.resetState() + } + } + + Box(modifier = Modifier.fillMaxWidth()) { + if (viewModel.isLoading) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = Color(0xFF4AB54A) // Hijau sesuai dengan tema + ) + } else if (viewModel.historyList.isEmpty()) { + // Pesan jika list kosong + Text( + text = "Belum ada riwayat makanan", + style = TextStyle( + fontSize = 16.sp, + fontFamily = medium, + color = Color.Gray + ), + modifier = Modifier + .align(Alignment.Center) + .padding(16.dp) + ) + } else { + // List riwayat kalori + LazyColumn( + modifier = Modifier.fillMaxWidth() + ) { + items(viewModel.historyList) { history -> + CaloryHistoryItem( + history = history, + onClick = { onItemClick(history) } + ) + Spacer(modifier = Modifier.height(12.dp)) + } + + // Spacer di akhir list untuk padding bottom + item { + Spacer(modifier = Modifier.height(70.dp)) + } + } + } + + // Error message + viewModel.errorMessage?.let { error -> + Text( + text = error, + color = Color.Red, + modifier = Modifier + .align(Alignment.Center) + .padding(16.dp) + ) + } + } +} + +/** + * Item riwayat kalori + */ +@Composable +fun CaloryHistoryItem( + history: CaloryHistoryModel, + onClick: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + elevation = 4.dp, + shape = RoundedCornerShape(16.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF4AB54A).copy(alpha = 0.6f)) // Warna hijau semi-transparan 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 + ) + ) + + Text( + text = "Lihat Detail", + style = TextStyle( + fontSize = 14.sp, + color = Color.White, + fontFamily = medium, + textDecoration = TextDecoration.Underline + ) + ) + } + + // Timestamp (opsional) + Text( + text = history.getFormattedDate(), + style = TextStyle( + fontSize = 12.sp, + color = Color.White.copy(alpha = 0.7f), + fontWeight = FontWeight.Normal + ) + ) + } + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76ebb1d..8930a42 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ firebaseAuth = "22.3.1" credentials = "1.5.0-rc01" credentialsPlayServicesAuth = "1.3.0" googleid = "1.1.1" +firebaseStorage = "21.0.1" #firebaseAuthKtx = "23.2.0" [libraries] @@ -63,6 +64,7 @@ firebase-auth = { group = "com.google.firebase", name = "firebase-auth", version androidx-credentials = { group = "androidx.credentials", name = "credentials", version.ref = "credentials" } androidx-credentials-play-services-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" } googleid = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleid" } +firebase-storage = { group = "com.google.firebase", name = "firebase-storage", version.ref = "firebaseStorage" } #firebase-auth-ktx = { group = "com.google.firebase", name = "firebase-auth-ktx", version.ref = "firebaseAuthKtx" } [plugins]