Progress 4
This commit is contained in:
parent
e13a914e28
commit
7cd246cea6
|
@ -1,4 +1,3 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="EntryPointsManager">
|
<component name="EntryPointsManager">
|
||||||
<list size="1">
|
<list size="1">
|
||||||
|
|
|
@ -70,6 +70,7 @@ dependencies {
|
||||||
implementation(libs.androidx.credentials)
|
implementation(libs.androidx.credentials)
|
||||||
implementation(libs.androidx.credentials.play.services.auth)
|
implementation(libs.androidx.credentials.play.services.auth)
|
||||||
implementation(libs.googleid)
|
implementation(libs.googleid)
|
||||||
|
implementation(libs.firebase.storage)
|
||||||
// implementation(libs.firebase.auth.ktx)
|
// implementation(libs.firebase.auth.ktx)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|
|
@ -7,18 +7,9 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.annotation.RequiresApi
|
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.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
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.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.pages.NavBarScreen
|
||||||
import com.example.caloryapp.ui.theme.CaloryAppTheme
|
import com.example.caloryapp.ui.theme.CaloryAppTheme
|
||||||
|
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,12 +9,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
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.MainScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileDetailScreen
|
import com.example.caloryapp.pages.account.ProfileDetailScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileScreen
|
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.ChangePasswordScreen
|
||||||
import com.example.caloryapp.pages.onboard.ForgotPasswordScreen
|
import com.example.caloryapp.pages.onboard.ForgotPasswordScreen
|
||||||
import com.example.caloryapp.pages.onboard.LoginScreen
|
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.RegisterScreen
|
||||||
import com.example.caloryapp.pages.onboard.SuccessChangePassword
|
import com.example.caloryapp.pages.onboard.SuccessChangePassword
|
||||||
import com.example.caloryapp.pages.onboard.SuccessRegister
|
import com.example.caloryapp.pages.onboard.SuccessRegister
|
||||||
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -30,6 +31,7 @@ fun Navigation(modifier: Modifier = Modifier) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val userViewModel: UserViewModel = viewModel()
|
val userViewModel: UserViewModel = viewModel()
|
||||||
val foodViewModel: FoodDetectionViewModel = viewModel()
|
val foodViewModel: FoodDetectionViewModel = viewModel()
|
||||||
|
val caloryHistoryViewModel: CaloryHistoryViewModel = viewModel()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
@ -70,7 +72,7 @@ fun Navigation(modifier: Modifier = Modifier) {
|
||||||
// HomeScreen(navController = navController, userViewModel)
|
// HomeScreen(navController = navController, userViewModel)
|
||||||
// }
|
// }
|
||||||
composable(NavigationScreen.MainScreen.name) {
|
composable(NavigationScreen.MainScreen.name) {
|
||||||
MainScreen(userViewModel, foodViewModel)
|
MainScreen(userViewModel, foodViewModel, caloryHistoryViewModel)
|
||||||
}
|
}
|
||||||
composable(NavigationScreen.ForgotPasswordScreen.name) {
|
composable(NavigationScreen.ForgotPasswordScreen.name) {
|
||||||
ForgotPasswordScreen(navController = navController)
|
ForgotPasswordScreen(navController = navController)
|
||||||
|
|
|
@ -35,18 +35,19 @@ import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.example.caloryapp.R
|
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.navigation.NavigationScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileDetailScreen
|
import com.example.caloryapp.pages.account.ProfileDetailScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileScreen
|
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.dashboard.HomeScreen
|
||||||
import com.example.caloryapp.pages.onboard.LoginScreen
|
import com.example.caloryapp.pages.onboard.LoginScreen
|
||||||
import com.example.caloryapp.ui.theme.bold
|
import com.example.caloryapp.ui.theme.bold
|
||||||
import com.example.caloryapp.ui.theme.medium
|
import com.example.caloryapp.ui.theme.medium
|
||||||
import com.example.caloryapp.ui.theme.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ -59,7 +60,8 @@ sealed class DrawerScreen(val title: String) {
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
userViewModel: UserViewModel,
|
userViewModel: UserViewModel,
|
||||||
foodDetectionViewModel: FoodDetectionViewModel
|
foodDetectionViewModel: FoodDetectionViewModel,
|
||||||
|
caloryHistoryViewModel: CaloryHistoryViewModel
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
@ -73,7 +75,7 @@ fun MainScreen(
|
||||||
) {
|
) {
|
||||||
NavHost(navController = navController, startDestination = DrawerScreen.HomeScreen.title) {
|
NavHost(navController = navController, startDestination = DrawerScreen.HomeScreen.title) {
|
||||||
composable(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) {
|
composable(DrawerScreen.ProfileScreen.title) {
|
||||||
ProfileScreen(navController = navController, drawerState = drawerState, scope = scope, viewModel = userViewModel)
|
ProfileScreen(navController = navController, drawerState = drawerState, scope = scope, viewModel = userViewModel)
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.Canvas
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
|
||||||
@Composable
|
@Composable
|
|
@ -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.annotation.OptIn
|
||||||
import androidx.camera.core.CameraSelector
|
|
||||||
import androidx.camera.core.ExperimentalGetImage
|
import androidx.camera.core.ExperimentalGetImage
|
||||||
import androidx.camera.core.ImageAnalysis
|
|
||||||
import androidx.camera.core.ImageProxy
|
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.nio.ByteBuffer
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
|
|
||||||
//@Composable
|
//@Composable
|
|
@ -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 android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.example.caloryapp.pages.camera
|
package com.example.caloryapp.pages.calorydetail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.tensorflow.lite.Interpreter
|
import org.tensorflow.lite.Interpreter
|
|
@ -1,4 +1,4 @@
|
||||||
package com.example.caloryapp.pages.camera
|
package com.example.caloryapp.pages.calorydetail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
@ -15,15 +15,12 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.tensorflow.lite.Interpreter
|
import org.tensorflow.lite.Interpreter
|
||||||
import org.tensorflow.lite.support.common.FileUtil
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,11 +14,17 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.FloatingActionButton
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material3.DrawerState
|
import androidx.compose.material3.DrawerState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
@ -28,6 +34,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
@ -35,7 +42,10 @@ import com.example.caloryapp.R
|
||||||
import com.example.caloryapp.navigation.NavigationScreen
|
import com.example.caloryapp.navigation.NavigationScreen
|
||||||
import com.example.caloryapp.ui.theme.background
|
import com.example.caloryapp.ui.theme.background
|
||||||
import com.example.caloryapp.ui.theme.bold
|
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.primary
|
||||||
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import com.example.caloryapp.widget.FilterBar
|
import com.example.caloryapp.widget.FilterBar
|
||||||
|
|
||||||
|
@ -45,11 +55,19 @@ fun HomeScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
drawerState: DrawerState,
|
drawerState: DrawerState,
|
||||||
scope: kotlinx.coroutines.CoroutineScope,
|
scope: kotlinx.coroutines.CoroutineScope,
|
||||||
|
caloryHistoryViewModel: CaloryHistoryViewModel,
|
||||||
viewModel: UserViewModel,
|
viewModel: UserViewModel,
|
||||||
) {
|
) {
|
||||||
var selectedFilter by remember { mutableStateOf("Semua") }
|
var selectedFilter by remember { mutableStateOf("Semua") }
|
||||||
val user = viewModel.user.value
|
val user = viewModel.user.value
|
||||||
|
|
||||||
|
LaunchedEffect(user) {
|
||||||
|
user?.let {
|
||||||
|
// Memuat 4 riwayat terbaru untuk ditampilkan di home
|
||||||
|
caloryHistoryViewModel.loadHistoryByUsername(it.username, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
@ -124,6 +142,84 @@ fun HomeScreen(
|
||||||
|
|
||||||
// Filter Bar
|
// Filter Bar
|
||||||
FilterBar(selectedFilter = selectedFilter, onFilterSelected = { selectedFilter = it })
|
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(
|
FloatingActionButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ firebaseAuth = "22.3.1"
|
||||||
credentials = "1.5.0-rc01"
|
credentials = "1.5.0-rc01"
|
||||||
credentialsPlayServicesAuth = "1.3.0"
|
credentialsPlayServicesAuth = "1.3.0"
|
||||||
googleid = "1.1.1"
|
googleid = "1.1.1"
|
||||||
|
firebaseStorage = "21.0.1"
|
||||||
#firebaseAuthKtx = "23.2.0"
|
#firebaseAuthKtx = "23.2.0"
|
||||||
|
|
||||||
[libraries]
|
[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 = { group = "androidx.credentials", name = "credentials", version.ref = "credentials" }
|
||||||
androidx-credentials-play-services-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" }
|
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" }
|
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" }
|
#firebase-auth-ktx = { group = "com.google.firebase", name = "firebase-auth-ktx", version.ref = "firebaseAuthKtx" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
Loading…
Reference in New Issue