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">
|
||||
<component name="EntryPointsManager">
|
||||
<list size="1">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.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
|
|
@ -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
|
|
@ -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
|
|
@ -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 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.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
|
|
@ -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.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
|
||||
|
|
|
@ -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"
|
||||
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]
|
||||
|
|
Loading…
Reference in New Issue