Progress 4

This commit is contained in:
E41212133_Naufal Kadhafi 2025-05-11 19:45:51 +07:00
parent e13a914e28
commit 7cd246cea6
21 changed files with 1610 additions and 367 deletions

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">

View File

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

View File

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

View File

@ -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<FoodDetectionResult?>(null)
private set
var isLoading by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
fun detectFoodFromImage(context: Context, uri: Uri) {
isLoading = true
errorMessage = null
viewModelScope.launch {
try {
val bitmap = loadBitmapFromUri(context, uri)
// Buat detector di sini, tidak dalam withContext
val detector = FoodDetector(context)
val result = withContext(Dispatchers.IO) {
detector.detectFood(bitmap)
}
detectionResult = result
} catch (e: IOException) {
Log.e("FoodViewModel", "Error IO: ${e.message}", e)
errorMessage = "Gagal memuat gambar atau model: ${e.message}"
} catch (e: Exception) {
Log.e("FoodViewModel", "Error umum: ${e.message}", e)
errorMessage = "Terjadi kesalahan: ${e.message}"
} finally {
isLoading = false
}
}
}
private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap {
val contentResolver = context.contentResolver
return contentResolver.openInputStream(uri).use { inputStream ->
android.graphics.BitmapFactory.decodeStream(inputStream)
} ?: throw IOException("Gagal membuka gambar")
}
}

View File

@ -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<String, Float> = mapOf(),
// Daftar kalori per kategori yang dihitung
val caloriesPerCategory: Map<String, Int> = 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"
}
}

View File

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

View File

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

View File

@ -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<String, Float> ke Map<FoodCategory, Float>
val foodCategories = history.foodComposition.mapKeys { entry ->
FoodCategory.values().find { it.displayName == entry.key } ?: FoodCategory.OTHER
}
Box(
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
PlateDiagram(
categories = foodCategories,
modifier = Modifier.size(200.dp)
)
}
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)
)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String?>(null)
// List riwayat kalori
var historyList by mutableStateOf<List<CaloryHistoryModel>>(emptyList())
// Detail riwayat kalori yang dipilih
var selectedHistory by mutableStateOf<CaloryHistoryModel?>(null)
/**
* Memuat riwayat kalori berdasarkan username
*/
fun loadHistoryByUsername(username: String, limit: Long = 10) {
isLoading = true
errorMessage = null
// Menggunakan viewModelScope, yang merupakan CoroutineScope yang otomatis dibatalkan saat ViewModel dihancurkan
viewModelScope.launch {
try {
// Membungkus fungsi callback-based dalam withContext untuk menjalankannya di thread IO
withContext(Dispatchers.IO) {
caloryRepository.getCaloryHistoryByUsername(username, limit) { histories ->
// Kembali ke thread utama untuk memperbarui UI
viewModelScope.launch(Dispatchers.Main) {
historyList = histories
isLoading = false
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
errorMessage = "Gagal memuat riwayat kalori: ${e.message}"
isLoading = false
}
}
}
}
/**
* Memuat detail riwayat kalori berdasarkan ID
*/
fun loadHistoryDetail(id: String) {
isLoading = true
errorMessage = null
viewModelScope.launch {
try {
withContext(Dispatchers.IO) {
caloryRepository.getCaloryHistoryById(id) { history ->
viewModelScope.launch(Dispatchers.Main) {
selectedHistory = history
isLoading = false
if (history == null) {
errorMessage = "Riwayat kalori tidak ditemukan"
}
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
errorMessage = "Gagal memuat detail riwayat: ${e.message}"
isLoading = false
}
}
}
}
/**
* Menghapus riwayat kalori
*/
fun deleteHistory(id: String, onComplete: (Boolean) -> Unit) {
isLoading = true
viewModelScope.launch {
try {
withContext(Dispatchers.IO) {
caloryRepository.deleteCaloryHistory(id) { success ->
viewModelScope.launch(Dispatchers.Main) {
isLoading = false
if (success) {
// Hapus dari list lokal jika berhasil
historyList = historyList.filter { it.id != id }
}
onComplete(success)
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
errorMessage = "Gagal menghapus riwayat: ${e.message}"
isLoading = false
onComplete(false)
}
}
}
}
/**
* Reset state
*/
fun resetState() {
historyList = emptyList()
selectedHistory = null
errorMessage = null
isLoading = false
}
}

View File

@ -0,0 +1,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<FoodDetectionResult?>(null)
// var errorMessage by mutableStateOf<String?>(null)
//
// // State untuk tracking penyimpanan
// var isSaving by mutableStateOf(false)
// var saveSuccess by mutableStateOf<Boolean?>(null)
// var lastSavedId by mutableStateOf<String?>(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<FoodDetectionResult?>(null)
private set
var isLoading by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
fun detectFoodFromImage(context: Context, uri: Uri) {
isLoading = true
errorMessage = null
viewModelScope.launch {
try {
val bitmap = loadBitmapFromUri(context, uri)
// Buat detector di sini, tidak dalam withContext
val detector = FoodDetector(context)
val result = withContext(Dispatchers.IO) {
detector.detectFood(bitmap)
}
detectionResult = result
} catch (e: IOException) {
Log.e("FoodViewModel", "Error IO: ${e.message}", e)
errorMessage = "Gagal memuat gambar atau model: ${e.message}"
} catch (e: Exception) {
Log.e("FoodViewModel", "Error umum: ${e.message}", e)
errorMessage = "Terjadi kesalahan: ${e.message}"
} finally {
isLoading = false
}
}
}
private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap {
val contentResolver = context.contentResolver
return contentResolver.openInputStream(uri).use { inputStream ->
android.graphics.BitmapFactory.decodeStream(inputStream)
} ?: throw IOException("Gagal membuka gambar")
}
}

View File

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

View File

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