Progress 3
This commit is contained in:
parent
0c8df7e4c7
commit
e13a914e28
|
@ -388,6 +388,17 @@
|
||||||
<option name="screenX" value="1008" />
|
<option name="screenX" value="1008" />
|
||||||
<option name="screenY" value="2244" />
|
<option name="screenY" value="2244" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="komodo" />
|
||||||
|
<option name="id" value="komodo" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro XL" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="1008" />
|
||||||
|
<option name="screenY" value="2244" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
|
|
@ -56,6 +56,7 @@ dependencies {
|
||||||
// implementation (libs.ohteepee)
|
// implementation (libs.ohteepee)
|
||||||
implementation (libs.ohteepee)
|
implementation (libs.ohteepee)
|
||||||
|
|
||||||
|
implementation("io.coil-kt.coil3:coil-compose:3.0.0")
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
|
@ -35,7 +35,6 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/Theme.CaloryApp">
|
android:theme="@style/Theme.CaloryApp">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
@ -43,6 +42,15 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
Binary file not shown.
Binary file not shown.
|
@ -1,20 +1,55 @@
|
||||||
package com.example.caloryapp
|
package com.example.caloryapp
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
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.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.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
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
// val foodClassifier = FoodClassifier(this)
|
// val foodClassifier = FoodClassifier(this)
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
android.Manifest.permission.READ_MEDIA_IMAGES
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(android.Manifest.permission.READ_MEDIA_IMAGES),
|
||||||
|
PERMISSION_REQUEST_CODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
CaloryAppTheme {
|
CaloryAppTheme {
|
||||||
|
// Surface(
|
||||||
|
// modifier = Modifier.fillMaxSize(),
|
||||||
|
// color = MaterialTheme.colorScheme.background
|
||||||
|
// ) {
|
||||||
|
// val viewModel = viewModel<FoodDetectionViewModel>()
|
||||||
|
// ScreenTest(viewModel)
|
||||||
|
// }
|
||||||
// val context = LocalContext.current
|
// val context = LocalContext.current
|
||||||
// var classificationResult by remember { mutableStateOf("Memuat...") }
|
// var classificationResult by remember { mutableStateOf("Memuat...") }
|
||||||
//
|
//
|
||||||
|
@ -27,10 +62,14 @@ class MainActivity : ComponentActivity() {
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
Navigation()
|
Navigation()
|
||||||
|
|
||||||
// LoginScreen(navController = rememberNavController())
|
// LoginScreen(navController = rememberNavController())
|
||||||
// ProfileScreen(navController = rememberNavController())
|
// ProfileScreen(navController = rememberNavController())
|
||||||
// MainScreen()
|
// MainScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
companion object {
|
||||||
|
private const val PERMISSION_REQUEST_CODE = 123
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.example.caloryapp.foodmodel
|
||||||
|
|
||||||
|
enum class FoodCategory(
|
||||||
|
val displayName: String,
|
||||||
|
val caloriesPer100g: Int,
|
||||||
|
val colorHex: String,
|
||||||
|
val icon: String
|
||||||
|
) {
|
||||||
|
CARBS("Karbohidrat", 150, "#FECD45", "🍚"), // Tetap 150 kal/100g
|
||||||
|
PROTEIN("Protein", 250, "#FC8369", "🍗"), // Ubah ke 250 kal/100g
|
||||||
|
VEGETABLES("Sayuran", 30, "#4AB54A", "🥦"), // Tetap 30 kal/100g
|
||||||
|
FRUITS("Buah", 60, "#FF6B6B", "🍎"), // Tetap 60 kal/100g
|
||||||
|
OTHER("Lainnya", 200, "#A0A0A0", "🍬") // Tetap 200 kal/100g
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FoodDetectionResult(
|
||||||
|
val mainCategory: FoodCategory,
|
||||||
|
val confidence: Float,
|
||||||
|
val allCategories: Map<FoodCategory, Float>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extension function untuk menghitung total kalori berdasarkan persentase makanan
|
||||||
|
fun Map<FoodCategory, Float>.calculateTotalCalories(plateWeightGrams: Int = 500): Int {
|
||||||
|
return this.entries.sumOf { (category, percentage) ->
|
||||||
|
val weightGrams = plateWeightGrams * percentage
|
||||||
|
(weightGrams * category.caloriesPer100g / 100).toInt()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
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,93 @@
|
||||||
|
package com.example.caloryapp.foodmodel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import org.tensorflow.lite.Interpreter
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
|
||||||
|
class FoodDetector(private val context: Context) {
|
||||||
|
private var interpreter: Interpreter? = null
|
||||||
|
private val categories = listOf(
|
||||||
|
FoodCategory.CARBS,
|
||||||
|
FoodCategory.PROTEIN,
|
||||||
|
FoodCategory.VEGETABLES,
|
||||||
|
FoodCategory.FRUITS,
|
||||||
|
FoodCategory.OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadModel() {
|
||||||
|
val assetFileDescriptor = context.assets.openFd("model_food_plate_densenet.tflite")
|
||||||
|
val fileInputStream = FileInputStream(assetFileDescriptor.fileDescriptor)
|
||||||
|
val fileChannel = fileInputStream.channel
|
||||||
|
val startOffset = assetFileDescriptor.startOffset
|
||||||
|
val declaredLength = assetFileDescriptor.declaredLength
|
||||||
|
val mappedByteBuffer = fileChannel.map(
|
||||||
|
FileChannel.MapMode.READ_ONLY,
|
||||||
|
startOffset, declaredLength
|
||||||
|
)
|
||||||
|
interpreter = Interpreter(mappedByteBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detectFood(bitmap: Bitmap): FoodDetectionResult {
|
||||||
|
// Resize bitmap to 128x128 (model input size)
|
||||||
|
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, 128, 128, true)
|
||||||
|
|
||||||
|
// Convert bitmap to ByteBuffer - Perbaikan disini
|
||||||
|
val modelInput = ByteBuffer.allocateDirect(128 * 128 * 3 * 4) // 4 bytes per float
|
||||||
|
modelInput.order(ByteOrder.nativeOrder())
|
||||||
|
|
||||||
|
// Citra harus dinormalisasi ke [0, 1] seperti di Python
|
||||||
|
val pixels = IntArray(128 * 128)
|
||||||
|
resizedBitmap.getPixels(pixels, 0, 128, 0, 0, 128, 128)
|
||||||
|
|
||||||
|
for (pixel in pixels) {
|
||||||
|
// Extract RGB values and normalize to [0, 1]
|
||||||
|
val r = (pixel shr 16 and 0xFF) / 255.0f
|
||||||
|
val g = (pixel shr 8 and 0xFF) / 255.0f
|
||||||
|
val b = (pixel and 0xFF) / 255.0f
|
||||||
|
|
||||||
|
// Memastikan urutan RGB sesuai dengan model
|
||||||
|
modelInput.putFloat(r)
|
||||||
|
modelInput.putFloat(g)
|
||||||
|
modelInput.putFloat(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
modelInput.rewind() // Penting: reset posisi buffer ke awal
|
||||||
|
|
||||||
|
// Run model - pastikan outputBuffer memiliki dimensi yang benar
|
||||||
|
val outputBuffer = Array(1) { FloatArray(5) } // 5 categories
|
||||||
|
interpreter?.run(modelInput, outputBuffer)
|
||||||
|
|
||||||
|
// Get predicted category
|
||||||
|
val confidences = outputBuffer[0]
|
||||||
|
val maxConfidenceIndex = confidences.indices.maxByOrNull { confidences[it] } ?: 0
|
||||||
|
|
||||||
|
// Membuat hasil yang lebih baik dengan threshold confidence
|
||||||
|
val minConfidenceThreshold = 0.4f // Atur sesuai kebutuhan
|
||||||
|
|
||||||
|
// Create detection result with confidence map
|
||||||
|
val detectedCategories = mutableMapOf<FoodCategory, Float>()
|
||||||
|
categories.forEachIndexed { index, category ->
|
||||||
|
detectedCategories[category] = confidences[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
return FoodDetectionResult(
|
||||||
|
mainCategory = categories[maxConfidenceIndex],
|
||||||
|
confidence = confidences[maxConfidenceIndex],
|
||||||
|
allCategories = detectedCategories
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
interpreter?.close()
|
||||||
|
interpreter = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.example.caloryapp.foodmodel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlateDiagram(
|
||||||
|
categories: Map<FoodCategory, Float>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
Canvas(modifier = Modifier.matchParentSize()) {
|
||||||
|
val canvasWidth = size.width
|
||||||
|
val canvasHeight = size.height
|
||||||
|
val radius = minOf(canvasWidth, canvasHeight) / 2
|
||||||
|
val center = androidx.compose.ui.geometry.Offset(canvasWidth / 2, canvasHeight / 2)
|
||||||
|
|
||||||
|
// Draw plate outline
|
||||||
|
drawCircle(
|
||||||
|
color = Color.LightGray,
|
||||||
|
radius = radius,
|
||||||
|
center = center,
|
||||||
|
style = Stroke(width = 2f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw food sections based on confidence
|
||||||
|
var startAngle = 0f
|
||||||
|
categories.forEach { (category, confidence) ->
|
||||||
|
val sweepAngle = 360f * confidence
|
||||||
|
val color = Color(android.graphics.Color.parseColor(category.colorHex))
|
||||||
|
|
||||||
|
drawArc(
|
||||||
|
color = color,
|
||||||
|
startAngle = startAngle,
|
||||||
|
sweepAngle = sweepAngle,
|
||||||
|
useCenter = true,
|
||||||
|
topLeft = center - androidx.compose.ui.geometry.Offset(radius, radius),
|
||||||
|
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
startAngle += sweepAngle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,37 @@
|
||||||
package com.example.caloryapp.navigation
|
package com.example.caloryapp.navigation
|
||||||
|
|
||||||
|
import androidx.compose.material3.DrawerValue
|
||||||
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.pages.onboard.LoginScreen
|
import com.example.caloryapp.foodmodel.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.ProfileDetailScreen
|
||||||
|
import com.example.caloryapp.pages.account.ProfileScreen
|
||||||
|
import com.example.caloryapp.pages.camera.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.OTPVerificationScreen
|
import com.example.caloryapp.pages.onboard.OTPVerificationScreen
|
||||||
import com.example.caloryapp.pages.onboard.OnBoardingScreen
|
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.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import com.example.caloryapp.pages.MainScreen
|
|
||||||
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Navigation(modifier: Modifier = Modifier) {
|
fun Navigation(modifier: Modifier = Modifier) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val userViewModel: UserViewModel = viewModel()
|
val userViewModel: UserViewModel = viewModel()
|
||||||
|
val foodViewModel: FoodDetectionViewModel = viewModel()
|
||||||
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
@ -43,11 +52,25 @@ fun Navigation(modifier: Modifier = Modifier) {
|
||||||
composable(NavigationScreen.ProfileChangePasswordScreen.name) {
|
composable(NavigationScreen.ProfileChangePasswordScreen.name) {
|
||||||
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
||||||
}
|
}
|
||||||
|
composable(NavigationScreen.ScreenTest.name) {
|
||||||
|
ScreenTest(navController = navController, viewModel = foodViewModel)
|
||||||
|
}
|
||||||
|
composable(NavigationScreen.ProfileScreen.name) {
|
||||||
|
ProfileScreen(
|
||||||
|
navController = navController,
|
||||||
|
viewModel = userViewModel,
|
||||||
|
scope = scope,
|
||||||
|
drawerState = drawerState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// composable(NavigationScreen.ScreenTest.name) {
|
||||||
|
// ScreenTest(navController = navController, viewModel = userViewModel)
|
||||||
|
// }
|
||||||
// composable(NavigationScreen.HomeScreen.name) {
|
// composable(NavigationScreen.HomeScreen.name) {
|
||||||
// HomeScreen(navController = navController, userViewModel)
|
// HomeScreen(navController = navController, userViewModel)
|
||||||
// }
|
// }
|
||||||
composable(NavigationScreen.MainScreen.name) {
|
composable(NavigationScreen.MainScreen.name) {
|
||||||
MainScreen(userViewModel)
|
MainScreen(userViewModel, foodViewModel)
|
||||||
}
|
}
|
||||||
composable(NavigationScreen.ForgotPasswordScreen.name) {
|
composable(NavigationScreen.ForgotPasswordScreen.name) {
|
||||||
ForgotPasswordScreen(navController = navController)
|
ForgotPasswordScreen(navController = navController)
|
||||||
|
|
|
@ -14,7 +14,8 @@ enum class NavigationScreen {
|
||||||
SuccessRegister,
|
SuccessRegister,
|
||||||
ProfileScreen,
|
ProfileScreen,
|
||||||
ProfileDetailScreen,
|
ProfileDetailScreen,
|
||||||
ProfileChangePasswordScreen;
|
ProfileChangePasswordScreen,
|
||||||
|
ScreenTest;
|
||||||
|
|
||||||
fun fromRoute(route: String): NavigationScreen =
|
fun fromRoute(route: String): NavigationScreen =
|
||||||
when (route.substringBefore("/")) {
|
when (route.substringBefore("/")) {
|
||||||
|
@ -32,6 +33,7 @@ enum class NavigationScreen {
|
||||||
SuccessRegister.name -> SuccessRegister
|
SuccessRegister.name -> SuccessRegister
|
||||||
ProfileDetailScreen.name -> ProfileDetailScreen
|
ProfileDetailScreen.name -> ProfileDetailScreen
|
||||||
ProfileChangePasswordScreen.name -> ProfileChangePasswordScreen
|
ProfileChangePasswordScreen.name -> ProfileChangePasswordScreen
|
||||||
|
ScreenTest.name -> ScreenTest
|
||||||
|
|
||||||
else -> throw IllegalArgumentException("$route gagal bji")
|
else -> throw IllegalArgumentException("$route gagal bji")
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,11 +35,14 @@ 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.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.dashboard.HomeScreen
|
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.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
|
||||||
|
@ -55,7 +58,8 @@ sealed class DrawerScreen(val title: String) {
|
||||||
@SuppressLint("UnusedMaterialScaffoldPaddingParameter", "UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterialScaffoldPaddingParameter", "UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
userViewModel: UserViewModel
|
userViewModel: UserViewModel,
|
||||||
|
foodDetectionViewModel: FoodDetectionViewModel
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
@ -80,6 +84,12 @@ fun MainScreen(
|
||||||
composable(NavigationScreen.ProfileChangePasswordScreen.name) {
|
composable(NavigationScreen.ProfileChangePasswordScreen.name) {
|
||||||
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
||||||
}
|
}
|
||||||
|
composable(NavigationScreen.ScreenTest.name) {
|
||||||
|
ScreenTest(navController = navController, viewModel = foodDetectionViewModel)
|
||||||
|
}
|
||||||
|
composable(NavigationScreen.LoginScreen.name) {
|
||||||
|
LoginScreen(navController = navController, viewModel = userViewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.example.caloryapp.pages.account
|
package com.example.caloryapp.pages.account
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
@ -20,42 +21,44 @@ import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material3.DrawerState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.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
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
import com.example.caloryapp.repository.UserRepository
|
||||||
import com.example.caloryapp.ui.theme.background
|
import com.example.caloryapp.ui.theme.background
|
||||||
|
import com.example.caloryapp.ui.theme.blueunderlined
|
||||||
import com.example.caloryapp.ui.theme.bold
|
import com.example.caloryapp.ui.theme.bold
|
||||||
import com.example.caloryapp.ui.theme.primaryblack
|
import com.example.caloryapp.ui.theme.primaryblack
|
||||||
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import com.example.caloryapp.widget.CustomTextField
|
import com.example.caloryapp.widget.CustomTextField
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileDetailScreen(
|
fun ProfileDetailScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
// drawerState: DrawerState,
|
|
||||||
// scope: kotlinx.coroutines.CoroutineScope,
|
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: UserViewModel
|
viewModel: UserViewModel
|
||||||
) {
|
) {
|
||||||
val user = viewModel.user.value
|
val user = viewModel.user.value
|
||||||
|
var username by remember { mutableStateOf(user!!.username) }
|
||||||
var username by remember { mutableStateOf("") }
|
var gmail by remember { mutableStateOf(user!!.email) }
|
||||||
var gmail by remember { mutableStateOf("") }
|
var fullName by remember { mutableStateOf(user!!.fullName) }
|
||||||
var fullName by remember { mutableStateOf("") }
|
var selectedGender by remember { mutableStateOf(user!!.gender) }
|
||||||
var password by remember { mutableStateOf("") }
|
var weight by remember { mutableStateOf(user!!.weight.toString()) }
|
||||||
var selectedGender by remember { mutableStateOf("") }
|
var height by remember { mutableStateOf(user!!.height.toString()) }
|
||||||
var weight by remember { mutableStateOf("") }
|
var isEditing by remember { mutableStateOf(false) } // Untuk toggle mode edit
|
||||||
var height by remember { mutableStateOf("") }
|
val userRepository = UserRepository()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -65,21 +68,16 @@ fun ProfileDetailScreen(
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 25.dp, vertical = 50.dp)
|
.padding(horizontal = 25.dp, vertical = 50.dp)
|
||||||
.verticalScroll(
|
.verticalScroll(rememberScrollState())
|
||||||
rememberScrollState()
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Spacer(modifier.height(50.dp))
|
Spacer(modifier.height(50.dp))
|
||||||
Row(
|
Row(Modifier.fillMaxWidth(), Arrangement.Start) {
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
Arrangement.Start,
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.KeyboardArrowLeft,
|
imageVector = Icons.Default.KeyboardArrowLeft,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
Modifier
|
Modifier
|
||||||
.size(28.dp)
|
.size(28.dp)
|
||||||
.clickable { navController.popBackStack()}
|
.clickable { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(30.dp))
|
Spacer(modifier = Modifier.width(30.dp))
|
||||||
Text(
|
Text(
|
||||||
|
@ -90,8 +88,56 @@ fun ProfileDetailScreen(
|
||||||
fontFamily = bold
|
fontFamily = bold
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(), Arrangement.End
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
if (username.isNotEmpty() && fullName.isNotEmpty() && gmail.isNotEmpty() && selectedGender.isNotEmpty() && weight.isNotEmpty() && height.isNotEmpty()) {
|
||||||
|
if (isEditing) {
|
||||||
|
// Jika tombol Edit ditekan dan dalam mode edit, simpan perubahan
|
||||||
|
userRepository.updateUserData(
|
||||||
|
username,
|
||||||
|
fullName,
|
||||||
|
gmail,
|
||||||
|
selectedGender,
|
||||||
|
weight,
|
||||||
|
height
|
||||||
|
) { success ->
|
||||||
|
if (success) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Data Berhasil Diubah",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
isEditing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isEditing = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Gagal",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
text = if (isEditing) "Simpan" else "Edit",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 20.sp,
|
||||||
|
color = blueunderlined,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(64.dp))
|
Spacer(modifier = Modifier.height(64.dp))
|
||||||
|
|
||||||
|
// Gambar profil
|
||||||
Row(Modifier.fillMaxWidth(), Arrangement.Center) {
|
Row(Modifier.fillMaxWidth(), Arrangement.Center) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.ic_profile_men),
|
painter = painterResource(id = R.drawable.ic_profile_men),
|
||||||
|
@ -101,6 +147,7 @@ fun ProfileDetailScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier.height(45.dp))
|
Spacer(modifier.height(45.dp))
|
||||||
|
// Nama Lengkap
|
||||||
Text(
|
Text(
|
||||||
text = "Nama Lengkap",
|
text = "Nama Lengkap",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -111,13 +158,14 @@ fun ProfileDetailScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(12.dp))
|
Spacer(modifier.height(12.dp))
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = user!!.fullName,
|
value = fullName,
|
||||||
onValueChange = { fullName = it },
|
onValueChange = { fullName = it },
|
||||||
placeholderText = "Masukkan Nama Lengkap",
|
placeholderText = "Masukkan Nama Lengkap",
|
||||||
input = false
|
input = isEditing
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
// Username
|
||||||
Text(
|
Text(
|
||||||
text = "Username",
|
text = "Username",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -128,13 +176,14 @@ fun ProfileDetailScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(12.dp))
|
Spacer(modifier.height(12.dp))
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = user.username,
|
value = username,
|
||||||
onValueChange = { },
|
onValueChange = { username = it },
|
||||||
placeholderText = "Masukkan Nama Lengkap",
|
placeholderText = "Masukkan Username",
|
||||||
input = false
|
input = false // Tidak dapat mengedit username
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
// Email
|
||||||
Text(
|
Text(
|
||||||
text = "Email",
|
text = "Email",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -145,13 +194,14 @@ fun ProfileDetailScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(12.dp))
|
Spacer(modifier.height(12.dp))
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = user.email,
|
value = gmail,
|
||||||
onValueChange = { },
|
onValueChange = { gmail = it },
|
||||||
placeholderText = "Masukkan Nama Lengkap",
|
placeholderText = "Masukkan Email",
|
||||||
input = false
|
input = isEditing
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
// Gender
|
||||||
Text(
|
Text(
|
||||||
text = "Gender",
|
text = "Gender",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -162,13 +212,14 @@ fun ProfileDetailScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(12.dp))
|
Spacer(modifier.height(12.dp))
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = user.gender,
|
value = selectedGender,
|
||||||
onValueChange = { },
|
onValueChange = { selectedGender = it },
|
||||||
placeholderText = "Masukkan Nama Lengkap",
|
placeholderText = "Masukkan Gender",
|
||||||
input = false
|
input = isEditing
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
// Berat Badan
|
||||||
Text(
|
Text(
|
||||||
text = "Berat Badan",
|
text = "Berat Badan",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -179,13 +230,14 @@ fun ProfileDetailScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(12.dp))
|
Spacer(modifier.height(12.dp))
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = "${user.weight} kg",
|
value = weight,
|
||||||
onValueChange = { },
|
onValueChange = { weight = it },
|
||||||
placeholderText = "Masukkan Nama Lengkap",
|
placeholderText = "Masukkan Berat Badan",
|
||||||
input = false
|
input = isEditing
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
// Tinggi Badan
|
||||||
Text(
|
Text(
|
||||||
text = "Tinggi Badan",
|
text = "Tinggi Badan",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -196,11 +248,15 @@ fun ProfileDetailScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(12.dp))
|
Spacer(modifier.height(12.dp))
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = "${user.height} cm",
|
value = height,
|
||||||
onValueChange = { },
|
onValueChange = { height = it },
|
||||||
placeholderText = "Masukkan Nama Lengkap",
|
placeholderText = "Masukkan Tinggi Badan",
|
||||||
input = false
|
input = isEditing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tombol Edit
|
||||||
|
Spacer(modifier.height(30.dp))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
@ -33,6 +34,7 @@ import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.os.unregisterForAllProfilingResults
|
import androidx.core.os.unregisterForAllProfilingResults
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
import com.example.caloryapp.navigation.Navigation
|
||||||
import com.example.caloryapp.navigation.NavigationScreen
|
import com.example.caloryapp.navigation.NavigationScreen
|
||||||
import com.example.caloryapp.pages.DrawerScreen
|
import com.example.caloryapp.pages.DrawerScreen
|
||||||
import com.example.caloryapp.ui.theme.background
|
import com.example.caloryapp.ui.theme.background
|
||||||
|
@ -45,11 +47,16 @@ import com.example.caloryapp.ui.theme.primaryred
|
||||||
import com.example.caloryapp.ui.theme.regular
|
import com.example.caloryapp.ui.theme.regular
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import com.example.caloryapp.widget.SimpleAlertDialog
|
import com.example.caloryapp.widget.SimpleAlertDialog
|
||||||
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
fun logoutUser() {
|
||||||
|
FirebaseAuth.getInstance().signOut()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(
|
fun ProfileScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -221,8 +228,9 @@ fun ProfileScreen(
|
||||||
dialogSubTitle = "Apakah Anda yakin ingin keluar?",
|
dialogSubTitle = "Apakah Anda yakin ingin keluar?",
|
||||||
onDismissRequest = { openAlertDialog.value = false },
|
onDismissRequest = { openAlertDialog.value = false },
|
||||||
onConfirmation = {
|
onConfirmation = {
|
||||||
|
logoutUser()
|
||||||
openAlertDialog.value = false
|
openAlertDialog.value = false
|
||||||
// Tambahkan aksi logout di sini
|
// Navigation()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,286 @@
|
||||||
|
package com.example.caloryapp.pages.camera
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
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.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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
|
||||||
|
import java.nio.MappedByteBuffer
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
|
||||||
|
// Model ViewModel
|
||||||
|
class FoodCalorieViewModel2 : ViewModel() {
|
||||||
|
// Kategori makanan dan informasi kalori
|
||||||
|
private val categoryNames = listOf("Karbohidrat", "Protein", "Sayur", "Buah", "Lainnya")
|
||||||
|
private val caloriesPerCategory = mapOf(
|
||||||
|
0 to 140, // Karbohidrat (nilai rata-rata 130-150)
|
||||||
|
1 to 225, // Protein (nilai rata-rata 200-250)
|
||||||
|
2 to 35, // Sayur (nilai rata-rata 25-50)
|
||||||
|
3 to 65, // Buah (nilai rata-rata 50-80)
|
||||||
|
4 to 350 // Lainnya (nilai rata-rata 300-400)
|
||||||
|
)
|
||||||
|
|
||||||
|
// State untuk UI
|
||||||
|
private val _selectedImageUri = mutableStateOf<Uri?>(null)
|
||||||
|
val selectedImageUri: State<Uri?> = _selectedImageUri
|
||||||
|
|
||||||
|
private val _predictionResult = mutableStateOf<PredictionResult?>(null)
|
||||||
|
val predictionResult: State<PredictionResult?> = _predictionResult
|
||||||
|
|
||||||
|
private val _isLoading = mutableStateOf(false)
|
||||||
|
val isLoading: State<Boolean> = _isLoading
|
||||||
|
|
||||||
|
// Fungsi untuk memuat model TFLite
|
||||||
|
private fun loadModelFile(context: Context): MappedByteBuffer {
|
||||||
|
return try {
|
||||||
|
val assetManager = context.assets
|
||||||
|
val modelPath = MODEL_PATH
|
||||||
|
val fileDescriptor = assetManager.openFd(modelPath)
|
||||||
|
|
||||||
|
FileInputStream(fileDescriptor.fileDescriptor).use { inputStream ->
|
||||||
|
val fileChannel = inputStream.channel
|
||||||
|
val startOffset = fileDescriptor.startOffset
|
||||||
|
val declaredLength = fileDescriptor.declaredLength
|
||||||
|
fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
throw RuntimeException("Error loading model: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fungsi untuk memproses gambar
|
||||||
|
private fun preprocessImage(bitmap: Bitmap): ByteBuffer {
|
||||||
|
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, 128, 128, true)
|
||||||
|
val byteBuffer = ByteBuffer.allocateDirect(4 * 128 * 128 * 3)
|
||||||
|
byteBuffer.order(ByteOrder.nativeOrder())
|
||||||
|
|
||||||
|
val pixels = IntArray(128 * 128)
|
||||||
|
resizedBitmap.getPixels(pixels, 0, 128, 0, 0, 128, 128)
|
||||||
|
|
||||||
|
for (pixel in pixels) {
|
||||||
|
// Normalisasi nilai RGB (0-255) ke (0-1)
|
||||||
|
byteBuffer.putFloat(((pixel shr 16) and 0xFF) / 255.0f) // R
|
||||||
|
byteBuffer.putFloat(((pixel shr 8) and 0xFF) / 255.0f) // G
|
||||||
|
byteBuffer.putFloat((pixel and 0xFF) / 255.0f) // B
|
||||||
|
}
|
||||||
|
|
||||||
|
return byteBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk melakukan prediksi
|
||||||
|
fun predictImage(context: Context, uri: Uri) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
_selectedImageUri.value = uri
|
||||||
|
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// Load gambar
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
|
||||||
|
// Preprocess gambar
|
||||||
|
val modelInput = preprocessImage(bitmap)
|
||||||
|
|
||||||
|
// Load model TFLite
|
||||||
|
val modelBuffer = loadModelFile(context)
|
||||||
|
val interpreter = Interpreter(modelBuffer)
|
||||||
|
|
||||||
|
// Persiapkan output
|
||||||
|
val outputBuffer = Array(1) { FloatArray(5) }
|
||||||
|
|
||||||
|
// Jalankan inferensi
|
||||||
|
interpreter.run(modelInput, outputBuffer)
|
||||||
|
|
||||||
|
// Interpretasi hasil
|
||||||
|
val outputArray = outputBuffer[0]
|
||||||
|
val maxIndex = outputArray.indices.maxByOrNull { outputArray[it] } ?: 0
|
||||||
|
val confidence = outputArray[maxIndex]
|
||||||
|
|
||||||
|
// Dapatkan kategori dan kalori
|
||||||
|
val category = categoryNames[maxIndex]
|
||||||
|
val calories = caloriesPerCategory[maxIndex] ?: 0
|
||||||
|
|
||||||
|
// Perbarui UI
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_predictionResult.value = PredictionResult(
|
||||||
|
category = category,
|
||||||
|
calories = calories,
|
||||||
|
confidence = confidence
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tutup interpreter
|
||||||
|
interpreter.close()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MODEL_PATH = "model_food_plate_densenet.tflite"
|
||||||
|
// "model_food_calories.tflite"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kelas untuk hasil prediksi
|
||||||
|
data class PredictionResult(
|
||||||
|
val category: String,
|
||||||
|
val calories: Int,
|
||||||
|
val confidence: Float
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Layar utama
|
||||||
|
@Composable
|
||||||
|
fun FoodCalorieScreen(viewModel: FoodCalorieViewModel2) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val selectedImageUri by viewModel.selectedImageUri
|
||||||
|
val predictionResult by viewModel.predictionResult
|
||||||
|
val isLoading by viewModel.isLoading
|
||||||
|
|
||||||
|
// Create an ActivityResultLauncher for picking images from gallery
|
||||||
|
val galleryLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
// Handle the returned Uri
|
||||||
|
uri?.let {
|
||||||
|
viewModel.predictImage(context, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Food Calorie Detector",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tampilkan gambar yang dipilih
|
||||||
|
selectedImageUri?.let { uri ->
|
||||||
|
val bitmap = remember(uri) {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
bitmap?.let {
|
||||||
|
Image(
|
||||||
|
bitmap = it.asImageBitmap(),
|
||||||
|
contentDescription = "Selected Food Image",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(200.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tombol pilih gambar (updated)
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
// Launch gallery to pick an image
|
||||||
|
galleryLauncher.launch("image/*")
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Text("Select Food Image")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan hasil prediksi
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
|
||||||
|
} else {
|
||||||
|
predictionResult?.let { result ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Detected Food: ${result.category}",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Estimated Calories: ${result.calories} kcal (per 100g)",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Confidence: ${(result.confidence * 100).toInt()}%",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Daily value: ${calculateDailyValue(result.category, result.calories)}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk menghitung persentase dari kebutuhan harian
|
||||||
|
fun calculateDailyValue(category: String, calories: Int): String {
|
||||||
|
// Asumsi kebutuhan harian rata-rata adalah 2000 kalori
|
||||||
|
val dailyCalorieNeed = 2000
|
||||||
|
|
||||||
|
// Asumsi porsi standar (dalam gram)
|
||||||
|
val standardPortion = when (category) {
|
||||||
|
"Karbohidrat" -> 150 // Misalnya 150g nasi
|
||||||
|
"Protein" -> 100 // Misalnya 100g daging/ikan
|
||||||
|
"Sayur" -> 200 // Misalnya 200g sayuran
|
||||||
|
"Buah" -> 150 // Misalnya 150g buah
|
||||||
|
"Lainnya" -> 50 // Misalnya 50g camilan
|
||||||
|
else -> 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kalori per porsi standar
|
||||||
|
val caloriesPerPortion = calories * standardPortion / 100
|
||||||
|
|
||||||
|
// Persentase dari kebutuhan harian
|
||||||
|
val percentage = (caloriesPerPortion * 100 / dailyCalorieNeed.toFloat()).toInt()
|
||||||
|
|
||||||
|
return "A standard portion ($standardPortion g) provides $caloriesPerPortion kcal, which is $percentage% of daily needs"
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
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,6 +14,8 @@ 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.material.FloatingActionButton
|
||||||
|
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
|
||||||
|
@ -25,26 +27,17 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
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
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
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.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
import com.example.caloryapp.ui.theme.primaryblack
|
|
||||||
import com.example.caloryapp.ui.theme.primarygrey
|
|
||||||
import com.example.caloryapp.widget.FilterBar
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.example.caloryapp.model.UserModel
|
|
||||||
import com.example.caloryapp.pages.camera.FoodClassifier
|
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import com.example.caloryapp.widget.FilterBar
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -132,5 +125,14 @@ fun HomeScreen(
|
||||||
// Filter Bar
|
// Filter Bar
|
||||||
FilterBar(selectedFilter = selectedFilter, onFilterSelected = { selectedFilter = it })
|
FilterBar(selectedFilter = selectedFilter, onFilterSelected = { selectedFilter = it })
|
||||||
}
|
}
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 50.dp, bottom = 70.dp),
|
||||||
|
contentColor = Color.White,
|
||||||
|
backgroundColor = primary,
|
||||||
|
onClick = { navController.navigate(NavigationScreen.ScreenTest.name) }) {
|
||||||
|
Icon(painter = painterResource(id = R.drawable.scan), contentDescription = null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,24 +1,35 @@
|
||||||
package com.example.caloryapp.pages.onboard
|
package com.example.caloryapp.pages.onboard
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
@ -27,36 +38,57 @@ import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
import com.example.caloryapp.navigation.NavigationScreen
|
import com.example.caloryapp.navigation.NavigationScreen
|
||||||
|
import com.example.caloryapp.repository.UserRepository
|
||||||
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.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
import com.example.caloryapp.ui.theme.primaryblack
|
import com.example.caloryapp.ui.theme.primaryblack
|
||||||
|
import com.example.caloryapp.widget.CustomPasswordTextField
|
||||||
import com.example.caloryapp.widget.CustomTextField
|
import com.example.caloryapp.widget.CustomTextField
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavController) {
|
fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavController) {
|
||||||
var newPassword by remember { mutableStateOf("") }
|
var newPassword by remember { mutableStateOf("") }
|
||||||
var confirmPassword by remember { mutableStateOf("") }
|
var confirmPassword by remember { mutableStateOf("") }
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var isLoading by remember { mutableStateOf(false) } // Untuk menampilkan status loading
|
||||||
|
val context = LocalContext.current
|
||||||
|
val userRepository = UserRepository()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(background)) {
|
.background(background)
|
||||||
|
) {
|
||||||
Column(Modifier.padding(horizontal = 25.dp, vertical = 50.dp)) {
|
Column(Modifier.padding(horizontal = 25.dp, vertical = 50.dp)) {
|
||||||
Spacer(Modifier.height(45.dp))
|
Spacer(Modifier.height(45.dp))
|
||||||
Row(Modifier.width(260.dp)) {
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.KeyboardArrowLeft,
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clickable { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(15.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.buat_kata_sandi_baru),
|
stringResource(R.string.buat_kata_sandi_baru),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 35.sp,
|
fontSize = 32.sp,
|
||||||
color = primaryblack,
|
color = primaryblack,
|
||||||
fontFamily = bold
|
fontFamily = bold
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
Spacer(modifier.height(42.dp))
|
Spacer(modifier.height(42.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = stringResource(R.string.masukkan_kata_sandi),
|
text = stringResource(R.string.username),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = primaryblack,
|
color = primaryblack,
|
||||||
|
@ -65,13 +97,14 @@ fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(16.dp))
|
Spacer(modifier.height(16.dp))
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = newPassword,
|
value = username,
|
||||||
onValueChange = { newPassword = it },input = true,
|
onValueChange = { username = it },
|
||||||
placeholderText = "Masukkan Kata Sandi"
|
input = true,
|
||||||
|
placeholderText = "Masukkan Username Anda"
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = stringResource(R.string.konfirmasi_kata_sandi),
|
text = "Kata Sandi Baru",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = primaryblack,
|
color = primaryblack,
|
||||||
|
@ -79,20 +112,81 @@ fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(16.dp))
|
Spacer(modifier.height(16.dp))
|
||||||
CustomTextField(
|
CustomPasswordTextField(
|
||||||
value = confirmPassword,
|
value = newPassword,
|
||||||
onValueChange = { confirmPassword = it },input = true,
|
onValueChange = { newPassword = it },
|
||||||
placeholderText = "Konfirmasi Kata Sandi"
|
placeholderText = "Masukkan Kata Sandi Baru",
|
||||||
|
input = true
|
||||||
)
|
)
|
||||||
|
Spacer(modifier.height(20.dp))
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = "Konfirmasi Kata Sandi Baru",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 20.sp,
|
||||||
|
color = primaryblack,
|
||||||
|
fontFamily = bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier.height(16.dp))
|
||||||
|
CustomPasswordTextField(
|
||||||
|
value = confirmPassword,
|
||||||
|
onValueChange = { confirmPassword = it },
|
||||||
|
placeholderText = "Konfirmasi Kata Sandi Baru",
|
||||||
|
input = true
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(45.dp))
|
Spacer(modifier.height(45.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { navController.navigate(NavigationScreen.SuccessChangePassword.name) },
|
onClick = {
|
||||||
|
if (username.isEmpty()) {
|
||||||
|
Toast.makeText(context, "Username Tidak Boleh Kosong!", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
} else if (newPassword.isEmpty()) {
|
||||||
|
Toast.makeText(context, "Masukkan Kata Sandi Terlebih Dahulu!", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
} else if (confirmPassword.isEmpty()) {
|
||||||
|
Toast.makeText(context, "Konfirmasi Kata Sandi Belum Diisi!", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
} else if (username.isNotEmpty() && newPassword.isNotEmpty() && confirmPassword.isNotEmpty()) {
|
||||||
|
if (newPassword == confirmPassword) {
|
||||||
|
isLoading = true // Menampilkan loading
|
||||||
|
userRepository.updatePasswordByUsername2(
|
||||||
|
username,
|
||||||
|
newPassword,
|
||||||
|
confirmPassword
|
||||||
|
) { success ->
|
||||||
|
isLoading = false // Menyembunyikan loading setelah selesai
|
||||||
|
if (success) {
|
||||||
|
// Navigasi ke layar sukses jika berhasil
|
||||||
|
navController.navigate(NavigationScreen.SuccessChangePassword.name)
|
||||||
|
} else {
|
||||||
|
// Tampilkan pesan kesalahan jika gagal
|
||||||
|
Log.e("ChangePassword", "Username Tidak Terdapat atau Tidak Sesuai")
|
||||||
|
Toast.makeText(context, "Username Tidak Terdapat atau Tidak Sesuai", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tampilkan pesan kesalahan jika kata sandi tidak cocok
|
||||||
|
Log.e("ChangePassword", "Kata sandi tidak cocok")
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Konfirmasi Kata sandi tidak cocok",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier
|
modifier
|
||||||
.width(360.dp)
|
.width(360.dp)
|
||||||
.height(50.dp),
|
.height(50.dp),
|
||||||
colors = androidx.compose.material.ButtonDefaults.buttonColors(backgroundColor = primary),
|
colors = androidx.compose.material.ButtonDefaults.buttonColors(backgroundColor = primary),
|
||||||
shape = RoundedCornerShape(20.dp)
|
shape = RoundedCornerShape(20.dp)
|
||||||
) {
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
androidx.compose.material3.CircularProgressIndicator(color = Color.White) // Loading indicator
|
||||||
|
} else {
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = "Simpan",
|
text = "Simpan",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -105,4 +199,5 @@ fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -15,7 +15,9 @@ 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.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
@ -48,6 +50,7 @@ import com.example.caloryapp.ui.theme.primaryblack
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
import com.example.caloryapp.viewmodel.LoginState
|
import com.example.caloryapp.viewmodel.LoginState
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
import com.example.caloryapp.widget.CustomPasswordTextField
|
||||||
import com.example.caloryapp.widget.CustomTextField
|
import com.example.caloryapp.widget.CustomTextField
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -78,7 +81,9 @@ fun LoginScreen(
|
||||||
.background(background)
|
.background(background)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier.padding(horizontal = 25.dp, vertical = 50.dp)
|
modifier
|
||||||
|
.padding(horizontal = 25.dp, vertical = 50.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Spacer(modifier.height(50.dp))
|
Spacer(modifier.height(50.dp))
|
||||||
Text(
|
Text(
|
||||||
|
@ -110,7 +115,7 @@ fun LoginScreen(
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it }, input = true,
|
onValueChange = { username = it }, input = true,
|
||||||
placeholderText = "Username"
|
placeholderText = "Masukkan Username"
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(16.dp))
|
Spacer(modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
|
@ -122,12 +127,30 @@ fun LoginScreen(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(16.dp))
|
Spacer(modifier.height(16.dp))
|
||||||
CustomTextField(
|
CustomPasswordTextField(
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { password = it }, input = true,
|
onValueChange = { password = it },
|
||||||
placeholderText = "Password"
|
placeholderText = "Masukkan Kata Sandi",
|
||||||
|
input = true
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(18.dp))
|
Spacer(modifier.height(18.dp))
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
Arrangement.End
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.clickable { navController.navigate(NavigationScreen.ChangePasswordScreen.name) },
|
||||||
|
text = "Lupa Password",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = blueunderlined,
|
||||||
|
fontFamily = semibold,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier.height(35.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (username.isEmpty()) {
|
if (username.isEmpty()) {
|
||||||
|
@ -151,7 +174,7 @@ fun LoginScreen(
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontFamily = MaterialTheme.typography.h1.fontFamily,
|
fontFamily = bold,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,6 +29,7 @@ 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
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
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.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
|
@ -42,7 +43,7 @@ fun SuccessChangePassword(modifier: Modifier = Modifier, navController: NavContr
|
||||||
Column(Modifier.padding(horizontal = 25.dp, vertical = 50.dp)) {
|
Column(Modifier.padding(horizontal = 25.dp, vertical = 50.dp)) {
|
||||||
Spacer(modifier = Modifier.height(50.dp))
|
Spacer(modifier = Modifier.height(50.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.password_kamu_berhasil_dibuat),
|
"Kata Sandi Kamu Berhasil Diubah",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 35.sp,
|
fontSize = 35.sp,
|
||||||
color = primaryblack,
|
color = primaryblack,
|
||||||
|
@ -58,7 +59,7 @@ fun SuccessChangePassword(modifier: Modifier = Modifier, navController: NavContr
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(115.dp))
|
Spacer(modifier = Modifier.height(115.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { },
|
onClick = { navController.navigate(NavigationScreen.LoginScreen.name) },
|
||||||
modifier
|
modifier
|
||||||
.width(360.dp)
|
.width(360.dp)
|
||||||
.height(50.dp),
|
.height(50.dp),
|
||||||
|
|
|
@ -20,6 +20,7 @@ class UserRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun registerUserWithCustomID(user: UserModel, userID: String, onComplete: (Boolean) -> Unit) {
|
fun registerUserWithCustomID(user: UserModel, userID: String, onComplete: (Boolean) -> Unit) {
|
||||||
db.collection("users")
|
db.collection("users")
|
||||||
.document(userID)
|
.document(userID)
|
||||||
|
@ -49,6 +50,10 @@ class UserRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logoutUser() {
|
||||||
|
FirebaseAuth.getInstance().signOut()
|
||||||
|
}
|
||||||
|
|
||||||
fun updatePasswordByUsername(username: String, oldPassword: String, newPassword: String, onComplete: (Boolean) -> Unit) {
|
fun updatePasswordByUsername(username: String, oldPassword: String, newPassword: String, onComplete: (Boolean) -> Unit) {
|
||||||
db.collection("users")
|
db.collection("users")
|
||||||
.whereEqualTo("username", username) // Mencari pengguna berdasarkan username
|
.whereEqualTo("username", username) // Mencari pengguna berdasarkan username
|
||||||
|
@ -80,6 +85,68 @@ class UserRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateUserData(username: String, fullName: String, email: String, gender: String, weight: String, height: String, onComplete: (Boolean) -> Unit) {
|
||||||
|
db.collection("users")
|
||||||
|
.whereEqualTo("username", username)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
if (!result.isEmpty) {
|
||||||
|
val userDocument = result.documents[0]
|
||||||
|
// Memperbarui data pengguna
|
||||||
|
userDocument.reference.update(
|
||||||
|
"fullName", fullName,
|
||||||
|
"email", email,
|
||||||
|
"gender", gender,
|
||||||
|
"weight", weight,
|
||||||
|
"height", height
|
||||||
|
)
|
||||||
|
.addOnSuccessListener {
|
||||||
|
onComplete(true) // Update berhasil
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
onComplete(false) // Update gagal
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onComplete(false) // Username tidak ditemukan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
onComplete(false) // Terjadi error dalam pencarian data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun updatePasswordByUsername2(username: String, newPassword: String, confirmPassword: String, onComplete: (Boolean) -> Unit) {
|
||||||
|
// Memeriksa apakah kata sandi baru dan konfirmasi cocok
|
||||||
|
if (newPassword != confirmPassword) {
|
||||||
|
onComplete(false) // Kata sandi tidak cocok
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mencari pengguna berdasarkan username
|
||||||
|
db.collection("users")
|
||||||
|
.whereEqualTo("username", username)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
if (!result.isEmpty) {
|
||||||
|
val userDocument = result.documents[0]
|
||||||
|
// Memperbarui kata sandi pengguna
|
||||||
|
userDocument.reference.update("password", newPassword)
|
||||||
|
.addOnSuccessListener {
|
||||||
|
onComplete(true) // Update berhasil
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
onComplete(false) // Update gagal
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onComplete(false) // Username tidak ditemukan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
onComplete(false) // Terjadi error dalam pencarian data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// private val db = FirebaseFirestore.getInstance()
|
// private val db = FirebaseFirestore.getInstance()
|
||||||
// private val auth = FirebaseAuth.getInstance()
|
// private val auth = FirebaseAuth.getInstance()
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@file:Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
|
||||||
package com.example.caloryapp.widget
|
package com.example.caloryapp.widget
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
@ -9,12 +11,16 @@ import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
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.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
import com.example.caloryapp.navigation.Navigation
|
||||||
|
import com.example.caloryapp.navigation.NavigationScreen
|
||||||
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.primaryblack
|
import com.example.caloryapp.ui.theme.primaryblack
|
||||||
|
@ -41,7 +47,9 @@ fun SimpleAlertDialog(
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { onConfirmation() }) {
|
TextButton(onClick = {
|
||||||
|
onConfirmation()
|
||||||
|
}) {
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = stringResource(R.string.ya),
|
text = stringResource(R.string.ya),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
package com.example.caloryapp.widget
|
package com.example.caloryapp.widget
|
||||||
|
|
||||||
|
//noinspection UsingMaterialAndMaterial3Libraries
|
||||||
|
//noinspection UsingMaterialAndMaterial3Libraries
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material3.Text
|
|
||||||
//noinspection UsingMaterialAndMaterial3Libraries
|
|
||||||
import androidx.compose.material.OutlinedTextField
|
import androidx.compose.material.OutlinedTextField
|
||||||
//noinspection UsingMaterialAndMaterial3Libraries
|
|
||||||
import androidx.compose.material.TextFieldDefaults
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
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 com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
@ -26,12 +29,6 @@ import com.example.caloryapp.ui.theme.bold
|
||||||
import com.example.caloryapp.ui.theme.primaryblack
|
import com.example.caloryapp.ui.theme.primaryblack
|
||||||
import com.example.caloryapp.ui.theme.primarygrey
|
import com.example.caloryapp.ui.theme.primarygrey
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Star
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomPasswordTextField(
|
fun CustomPasswordTextField(
|
||||||
|
@ -66,16 +63,23 @@ fun CustomPasswordTextField(
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(50.dp)
|
.height(50.dp), // Add padding for the icon
|
||||||
.padding(end = 16.dp), // Add padding for the icon
|
|
||||||
visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) {
|
IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isPasswordVisible) Icons.Filled.Check else Icons.Filled.Check,
|
modifier = Modifier.size(25.dp),
|
||||||
contentDescription = "Toggle Password Visibility",
|
painter = if (isPasswordVisible) painterResource(id = R.drawable.ic_show) else painterResource(
|
||||||
tint = Color.Gray // Sesuaikan dengan warna ikon pada desain
|
id = R.drawable.ic_hide
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Gray
|
||||||
)
|
)
|
||||||
|
// Icon(
|
||||||
|
// imageVector = if (isPasswordVisible) Icons.Filled.Check else Icons.Filled.Check,
|
||||||
|
// contentDescription = "Toggle Password Visibility",
|
||||||
|
// tint = Color.Gray // Sesuaikan dengan warna ikon pada desain
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<files-path
|
||||||
|
name="my_images"
|
||||||
|
path="/" />
|
||||||
|
</paths>
|
Loading…
Reference in New Issue