Progress 3
This commit is contained in:
parent
0c8df7e4c7
commit
e13a914e28
|
@ -388,6 +388,17 @@
|
|||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</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>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
|
|
|
@ -56,6 +56,7 @@ dependencies {
|
|||
// implementation (libs.ohteepee)
|
||||
implementation (libs.ohteepee)
|
||||
|
||||
implementation("io.coil-kt.coil3:coil-compose:3.0.0")
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.CaloryApp">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
@ -43,6 +42,15 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</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>
|
||||
|
||||
</manifest>
|
Binary file not shown.
Binary file not shown.
|
@ -1,20 +1,55 @@
|
|||
package com.example.caloryapp
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
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
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
// 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 {
|
||||
CaloryAppTheme {
|
||||
// Surface(
|
||||
// modifier = Modifier.fillMaxSize(),
|
||||
// color = MaterialTheme.colorScheme.background
|
||||
// ) {
|
||||
// val viewModel = viewModel<FoodDetectionViewModel>()
|
||||
// ScreenTest(viewModel)
|
||||
// }
|
||||
// val context = LocalContext.current
|
||||
// var classificationResult by remember { mutableStateOf("Memuat...") }
|
||||
//
|
||||
|
@ -27,10 +62,14 @@ class MainActivity : ComponentActivity() {
|
|||
// })
|
||||
// }
|
||||
Navigation()
|
||||
|
||||
// LoginScreen(navController = rememberNavController())
|
||||
// ProfileScreen(navController = rememberNavController())
|
||||
// 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
|
||||
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
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.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.ProfileScreen
|
||||
import com.example.caloryapp.pages.camera.ScreenTest
|
||||
import com.example.caloryapp.pages.onboard.ChangePasswordScreen
|
||||
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.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.UserViewModel
|
||||
import com.example.caloryapp.pages.MainScreen
|
||||
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
||||
|
||||
@Composable
|
||||
fun Navigation(modifier: Modifier = Modifier) {
|
||||
val navController = rememberNavController()
|
||||
val userViewModel: UserViewModel = viewModel()
|
||||
val foodViewModel: FoodDetectionViewModel = viewModel()
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
|
@ -43,11 +52,25 @@ fun Navigation(modifier: Modifier = Modifier) {
|
|||
composable(NavigationScreen.ProfileChangePasswordScreen.name) {
|
||||
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) {
|
||||
// HomeScreen(navController = navController, userViewModel)
|
||||
// }
|
||||
composable(NavigationScreen.MainScreen.name) {
|
||||
MainScreen(userViewModel)
|
||||
MainScreen(userViewModel, foodViewModel)
|
||||
}
|
||||
composable(NavigationScreen.ForgotPasswordScreen.name) {
|
||||
ForgotPasswordScreen(navController = navController)
|
||||
|
|
|
@ -14,7 +14,8 @@ enum class NavigationScreen {
|
|||
SuccessRegister,
|
||||
ProfileScreen,
|
||||
ProfileDetailScreen,
|
||||
ProfileChangePasswordScreen;
|
||||
ProfileChangePasswordScreen,
|
||||
ScreenTest;
|
||||
|
||||
fun fromRoute(route: String): NavigationScreen =
|
||||
when (route.substringBefore("/")) {
|
||||
|
@ -32,6 +33,7 @@ enum class NavigationScreen {
|
|||
SuccessRegister.name -> SuccessRegister
|
||||
ProfileDetailScreen.name -> ProfileDetailScreen
|
||||
ProfileChangePasswordScreen.name -> ProfileChangePasswordScreen
|
||||
ScreenTest.name -> ScreenTest
|
||||
|
||||
else -> throw IllegalArgumentException("$route gagal bji")
|
||||
}
|
||||
|
|
|
@ -35,11 +35,14 @@ 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.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.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
|
||||
|
@ -55,7 +58,8 @@ sealed class DrawerScreen(val title: String) {
|
|||
@SuppressLint("UnusedMaterialScaffoldPaddingParameter", "UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
userViewModel: UserViewModel
|
||||
userViewModel: UserViewModel,
|
||||
foodDetectionViewModel: FoodDetectionViewModel
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
|
@ -80,6 +84,12 @@ fun MainScreen(
|
|||
composable(NavigationScreen.ProfileChangePasswordScreen.name) {
|
||||
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
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
@ -20,42 +21,44 @@ import androidx.compose.material.Icon
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material3.DrawerState
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.caloryapp.R
|
||||
import com.example.caloryapp.repository.UserRepository
|
||||
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.primaryblack
|
||||
import com.example.caloryapp.ui.theme.semibold
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import com.example.caloryapp.widget.CustomTextField
|
||||
|
||||
@Composable
|
||||
fun ProfileDetailScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
// drawerState: DrawerState,
|
||||
// scope: kotlinx.coroutines.CoroutineScope,
|
||||
navController: NavController,
|
||||
viewModel: UserViewModel
|
||||
) {
|
||||
val user = viewModel.user.value
|
||||
|
||||
var username by remember { mutableStateOf("") }
|
||||
var gmail by remember { mutableStateOf("") }
|
||||
var fullName by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var selectedGender by remember { mutableStateOf("") }
|
||||
var weight by remember { mutableStateOf("") }
|
||||
var height by remember { mutableStateOf("") }
|
||||
var username by remember { mutableStateOf(user!!.username) }
|
||||
var gmail by remember { mutableStateOf(user!!.email) }
|
||||
var fullName by remember { mutableStateOf(user!!.fullName) }
|
||||
var selectedGender by remember { mutableStateOf(user!!.gender) }
|
||||
var weight by remember { mutableStateOf(user!!.weight.toString()) }
|
||||
var height by remember { mutableStateOf(user!!.height.toString()) }
|
||||
var isEditing by remember { mutableStateOf(false) } // Untuk toggle mode edit
|
||||
val userRepository = UserRepository()
|
||||
val context = LocalContext.current
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
|
@ -65,21 +68,16 @@ fun ProfileDetailScreen(
|
|||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 25.dp, vertical = 50.dp)
|
||||
.verticalScroll(
|
||||
rememberScrollState()
|
||||
)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(modifier.height(50.dp))
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
Arrangement.Start,
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth(), Arrangement.Start) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowLeft,
|
||||
contentDescription = null,
|
||||
Modifier
|
||||
.size(28.dp)
|
||||
.clickable { navController.popBackStack()}
|
||||
.clickable { navController.popBackStack() }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(30.dp))
|
||||
Text(
|
||||
|
@ -90,8 +88,56 @@ fun ProfileDetailScreen(
|
|||
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))
|
||||
|
||||
// Gambar profil
|
||||
Row(Modifier.fillMaxWidth(), Arrangement.Center) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_profile_men),
|
||||
|
@ -101,6 +147,7 @@ fun ProfileDetailScreen(
|
|||
}
|
||||
|
||||
Spacer(modifier.height(45.dp))
|
||||
// Nama Lengkap
|
||||
Text(
|
||||
text = "Nama Lengkap",
|
||||
style = TextStyle(
|
||||
|
@ -111,13 +158,14 @@ fun ProfileDetailScreen(
|
|||
)
|
||||
Spacer(modifier.height(12.dp))
|
||||
CustomTextField(
|
||||
value = user!!.fullName,
|
||||
value = fullName,
|
||||
onValueChange = { fullName = it },
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
input = false
|
||||
input = isEditing
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
// Username
|
||||
Text(
|
||||
text = "Username",
|
||||
style = TextStyle(
|
||||
|
@ -128,13 +176,14 @@ fun ProfileDetailScreen(
|
|||
)
|
||||
Spacer(modifier.height(12.dp))
|
||||
CustomTextField(
|
||||
value = user.username,
|
||||
onValueChange = { },
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
input = false
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
placeholderText = "Masukkan Username",
|
||||
input = false // Tidak dapat mengedit username
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
// Email
|
||||
Text(
|
||||
text = "Email",
|
||||
style = TextStyle(
|
||||
|
@ -145,13 +194,14 @@ fun ProfileDetailScreen(
|
|||
)
|
||||
Spacer(modifier.height(12.dp))
|
||||
CustomTextField(
|
||||
value = user.email,
|
||||
onValueChange = { },
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
input = false
|
||||
value = gmail,
|
||||
onValueChange = { gmail = it },
|
||||
placeholderText = "Masukkan Email",
|
||||
input = isEditing
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
// Gender
|
||||
Text(
|
||||
text = "Gender",
|
||||
style = TextStyle(
|
||||
|
@ -162,13 +212,14 @@ fun ProfileDetailScreen(
|
|||
)
|
||||
Spacer(modifier.height(12.dp))
|
||||
CustomTextField(
|
||||
value = user.gender,
|
||||
onValueChange = { },
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
input = false
|
||||
value = selectedGender,
|
||||
onValueChange = { selectedGender = it },
|
||||
placeholderText = "Masukkan Gender",
|
||||
input = isEditing
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
// Berat Badan
|
||||
Text(
|
||||
text = "Berat Badan",
|
||||
style = TextStyle(
|
||||
|
@ -179,13 +230,14 @@ fun ProfileDetailScreen(
|
|||
)
|
||||
Spacer(modifier.height(12.dp))
|
||||
CustomTextField(
|
||||
value = "${user.weight} kg",
|
||||
onValueChange = { },
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
input = false
|
||||
value = weight,
|
||||
onValueChange = { weight = it },
|
||||
placeholderText = "Masukkan Berat Badan",
|
||||
input = isEditing
|
||||
)
|
||||
|
||||
Spacer(modifier.height(20.dp))
|
||||
// Tinggi Badan
|
||||
Text(
|
||||
text = "Tinggi Badan",
|
||||
style = TextStyle(
|
||||
|
@ -196,11 +248,15 @@ fun ProfileDetailScreen(
|
|||
)
|
||||
Spacer(modifier.height(12.dp))
|
||||
CustomTextField(
|
||||
value = "${user.height} cm",
|
||||
onValueChange = { },
|
||||
placeholderText = "Masukkan Nama Lengkap",
|
||||
input = false
|
||||
value = height,
|
||||
onValueChange = { height = it },
|
||||
placeholderText = "Masukkan Tinggi Badan",
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
|
@ -33,6 +34,7 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.core.os.unregisterForAllProfilingResults
|
||||
import androidx.navigation.NavController
|
||||
import com.example.caloryapp.R
|
||||
import com.example.caloryapp.navigation.Navigation
|
||||
import com.example.caloryapp.navigation.NavigationScreen
|
||||
import com.example.caloryapp.pages.DrawerScreen
|
||||
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.viewmodel.UserViewModel
|
||||
import com.example.caloryapp.widget.SimpleAlertDialog
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
fun logoutUser() {
|
||||
FirebaseAuth.getInstance().signOut()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -221,8 +228,9 @@ fun ProfileScreen(
|
|||
dialogSubTitle = "Apakah Anda yakin ingin keluar?",
|
||||
onDismissRequest = { openAlertDialog.value = false },
|
||||
onConfirmation = {
|
||||
logoutUser()
|
||||
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.size
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -25,26 +27,17 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
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.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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.caloryapp.widget.FilterBar
|
||||
|
||||
|
||||
@Composable
|
||||
|
@ -71,7 +64,7 @@ fun HomeScreen(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.clickable { },
|
||||
modifier = Modifier.clickable { },
|
||||
painter = painterResource(id = R.drawable.ic_home_acc),
|
||||
contentDescription = null
|
||||
)
|
||||
|
@ -132,5 +125,14 @@ fun HomeScreen(
|
|||
// Filter Bar
|
||||
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
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
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.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.shape.RoundedCornerShape
|
||||
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.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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -27,36 +38,57 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.navigation.NavController
|
||||
import com.example.caloryapp.R
|
||||
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.bold
|
||||
import com.example.caloryapp.ui.theme.primary
|
||||
import com.example.caloryapp.ui.theme.primaryblack
|
||||
import com.example.caloryapp.widget.CustomPasswordTextField
|
||||
import com.example.caloryapp.widget.CustomTextField
|
||||
|
||||
@Composable
|
||||
fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavController) {
|
||||
var newPassword 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(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(background)) {
|
||||
.background(background)
|
||||
) {
|
||||
Column(Modifier.padding(horizontal = 25.dp, vertical = 50.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(
|
||||
stringResource(R.string.buat_kata_sandi_baru),
|
||||
style = TextStyle(
|
||||
fontSize = 35.sp,
|
||||
fontSize = 32.sp,
|
||||
color = primaryblack,
|
||||
fontFamily = bold
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Spacer(modifier.height(42.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.masukkan_kata_sandi),
|
||||
text = stringResource(R.string.username),
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = primaryblack,
|
||||
|
@ -65,13 +97,14 @@ fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
|||
)
|
||||
Spacer(modifier.height(16.dp))
|
||||
CustomTextField(
|
||||
value = newPassword,
|
||||
onValueChange = { newPassword = it },input = true,
|
||||
placeholderText = "Masukkan Kata Sandi"
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
input = true,
|
||||
placeholderText = "Masukkan Username Anda"
|
||||
)
|
||||
Spacer(modifier.height(20.dp))
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.konfirmasi_kata_sandi),
|
||||
text = "Kata Sandi Baru",
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = primaryblack,
|
||||
|
@ -79,30 +112,92 @@ fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
|||
)
|
||||
)
|
||||
Spacer(modifier.height(16.dp))
|
||||
CustomTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },input = true,
|
||||
placeholderText = "Konfirmasi Kata Sandi"
|
||||
CustomPasswordTextField(
|
||||
value = newPassword,
|
||||
onValueChange = { newPassword = it },
|
||||
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))
|
||||
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
|
||||
.width(360.dp)
|
||||
.height(50.dp),
|
||||
colors = androidx.compose.material.ButtonDefaults.buttonColors(backgroundColor = primary),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
) {
|
||||
androidx.compose.material.Text(
|
||||
text = "Simpan",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.White,
|
||||
fontFamily = bold,
|
||||
textAlign = TextAlign.Center
|
||||
if (isLoading) {
|
||||
androidx.compose.material3.CircularProgressIndicator(color = Color.White) // Loading indicator
|
||||
} else {
|
||||
androidx.compose.material.Text(
|
||||
text = "Simpan",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.White,
|
||||
fontFamily = bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ 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.MaterialTheme
|
||||
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.viewmodel.LoginState
|
||||
import com.example.caloryapp.viewmodel.UserViewModel
|
||||
import com.example.caloryapp.widget.CustomPasswordTextField
|
||||
import com.example.caloryapp.widget.CustomTextField
|
||||
|
||||
@Composable
|
||||
|
@ -78,7 +81,9 @@ fun LoginScreen(
|
|||
.background(background)
|
||||
) {
|
||||
Column(
|
||||
modifier.padding(horizontal = 25.dp, vertical = 50.dp)
|
||||
modifier
|
||||
.padding(horizontal = 25.dp, vertical = 50.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(modifier.height(50.dp))
|
||||
Text(
|
||||
|
@ -110,7 +115,7 @@ fun LoginScreen(
|
|||
CustomTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it }, input = true,
|
||||
placeholderText = "Username"
|
||||
placeholderText = "Masukkan Username"
|
||||
)
|
||||
Spacer(modifier.height(16.dp))
|
||||
Text(
|
||||
|
@ -122,12 +127,30 @@ fun LoginScreen(
|
|||
)
|
||||
)
|
||||
Spacer(modifier.height(16.dp))
|
||||
CustomTextField(
|
||||
CustomPasswordTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it }, input = true,
|
||||
placeholderText = "Password"
|
||||
onValueChange = { password = it },
|
||||
placeholderText = "Masukkan Kata Sandi",
|
||||
input = true
|
||||
)
|
||||
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(
|
||||
onClick = {
|
||||
if (username.isEmpty()) {
|
||||
|
@ -151,7 +174,7 @@ fun LoginScreen(
|
|||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.White,
|
||||
fontFamily = MaterialTheme.typography.h1.fontFamily,
|
||||
fontFamily = bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
|
|
|
@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
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.primary
|
||||
|
@ -42,7 +43,7 @@ fun SuccessChangePassword(modifier: Modifier = Modifier, navController: NavContr
|
|||
Column(Modifier.padding(horizontal = 25.dp, vertical = 50.dp)) {
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
Text(
|
||||
stringResource(R.string.password_kamu_berhasil_dibuat),
|
||||
"Kata Sandi Kamu Berhasil Diubah",
|
||||
style = TextStyle(
|
||||
fontSize = 35.sp,
|
||||
color = primaryblack,
|
||||
|
@ -58,7 +59,7 @@ fun SuccessChangePassword(modifier: Modifier = Modifier, navController: NavContr
|
|||
}
|
||||
Spacer(modifier = Modifier.height(115.dp))
|
||||
Button(
|
||||
onClick = { },
|
||||
onClick = { navController.navigate(NavigationScreen.LoginScreen.name) },
|
||||
modifier
|
||||
.width(360.dp)
|
||||
.height(50.dp),
|
||||
|
|
|
@ -20,6 +20,7 @@ class UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fun registerUserWithCustomID(user: UserModel, userID: String, onComplete: (Boolean) -> Unit) {
|
||||
db.collection("users")
|
||||
.document(userID)
|
||||
|
@ -49,6 +50,10 @@ class UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
fun logoutUser() {
|
||||
FirebaseAuth.getInstance().signOut()
|
||||
}
|
||||
|
||||
fun updatePasswordByUsername(username: String, oldPassword: String, newPassword: String, onComplete: (Boolean) -> Unit) {
|
||||
db.collection("users")
|
||||
.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 auth = FirebaseAuth.getInstance()
|
||||
//
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("CAST_NEVER_SUCCEEDS")
|
||||
|
||||
package com.example.caloryapp.widget
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
@ -9,12 +11,16 @@ import androidx.compose.material3.TextButton
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.NavController
|
||||
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.medium
|
||||
import com.example.caloryapp.ui.theme.primaryblack
|
||||
|
@ -41,7 +47,9 @@ fun SimpleAlertDialog(
|
|||
onDismissRequest()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onConfirmation() }) {
|
||||
TextButton(onClick = {
|
||||
onConfirmation()
|
||||
}) {
|
||||
androidx.compose.material.Text(
|
||||
text = stringResource(R.string.ya),
|
||||
style = TextStyle(
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
package com.example.caloryapp.widget
|
||||
|
||||
//noinspection UsingMaterialAndMaterial3Libraries
|
||||
//noinspection UsingMaterialAndMaterial3Libraries
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
//noinspection UsingMaterialAndMaterial3Libraries
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
//noinspection UsingMaterialAndMaterial3Libraries
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
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.Modifier
|
||||
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.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.primarygrey
|
||||
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
|
||||
fun CustomPasswordTextField(
|
||||
|
@ -66,16 +63,23 @@ fun CustomPasswordTextField(
|
|||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.padding(end = 16.dp), // Add padding for the icon
|
||||
.height(50.dp), // Add padding for the icon
|
||||
visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) {
|
||||
Icon(
|
||||
imageVector = if (isPasswordVisible) Icons.Filled.Check else Icons.Filled.Check,
|
||||
contentDescription = "Toggle Password Visibility",
|
||||
tint = Color.Gray // Sesuaikan dengan warna ikon pada desain
|
||||
modifier = Modifier.size(25.dp),
|
||||
painter = if (isPasswordVisible) painterResource(id = R.drawable.ic_show) else painterResource(
|
||||
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(
|
||||
|
|
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