diff --git a/app/src/main/java/com/example/tiptime/MainActivity.kt b/app/src/main/java/com/example/tiptime/MainActivity.kt index 1d607a4..f932db9 100644 --- a/app/src/main/java/com/example/tiptime/MainActivity.kt +++ b/app/src/main/java/com/example/tiptime/MainActivity.kt @@ -1,30 +1,251 @@ +package com.example.tiptime + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +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.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.error +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.tiptime.ui.theme.TipTimeTheme -// --- Composable InputWithImage DIPERBARUI dengan Batas Karakter --- +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + setContent { + TipTimeTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.surfaceContainerLowest + ) { + NavigationController() + } + } + } + } +} + +// Navigasi tidak berubah +private sealed class Screen { + object Start : Screen() + object Main : Screen() +} + +@Composable +private fun NavigationController() { + var currentScreen by remember { mutableStateOf(Screen.Start) } + + when (currentScreen) { + is Screen.Start -> { + StartScreen(onNavigateToMain = { currentScreen = Screen.Main }) + } + is Screen.Main -> { + BMICalculatorScreen() + } + } +} + + +// --- VERSI DENGAN TOMBOL HITUNG --- +@Composable +fun BMICalculatorScreen() { + // --- State untuk input pengguna --- + var heightInput by remember { mutableStateOf("") } + var weightInput by remember { mutableStateOf("") } + var useMetric by remember { mutableStateOf(true) } + + // --- State untuk hasil perhitungan (dipisah) --- + var calculatedBmi by remember { mutableStateOf(0.0) } + + // State untuk status error + var isHeightError by remember { mutableStateOf(false) } + var isWeightError by remember { mutableStateOf(false) } + + // Batas Maksimal + val maxHeightCm = 250.0 + val maxWeightKg = 300.0 + + // Keyboard controller untuk menyembunyikan keyboard setelah tombol ditekan + val keyboardController = LocalSoftwareKeyboardController.current + + // --- FUNGSI BARU YANG DIPANGGIL SAAT TOMBOL DITEKAN --- + fun performCalculation() { + keyboardController?.hide() // Sembunyikan keyboard + + val height = heightInput.toDoubleOrNull() ?: 0.0 + val weight = weightInput.toDoubleOrNull() ?: 0.0 + + // Lakukan validasi saat tombol ditekan + isHeightError = height <= 0 || height > maxHeightCm + isWeightError = weight <= 0 || weight > maxWeightKg + + // Hanya hitung dan perbarui state hasil jika tidak ada error + if (!isHeightError && !isWeightError) { + calculatedBmi = calculateBMI(height, weight, useMetric) + } else { + calculatedBmi = 0.0 // Reset hasil jika ada error + } + } + + // Menggunakan state hasil (calculatedBmi) untuk tampilan kartu atas + val bmiCategory = bmiCategory(calculatedBmi) + val bmiCategoryColor = getBmiCategoryColor(bmiCategory) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .safeDrawingPadding() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.bmi_calculator), + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 16.dp) + ) + + // --- Kartu Hasil BMI --- + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.your_bmi_is), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + // Menampilkan dari state hasil 'calculatedBmi' + text = "%.1f".format(calculatedBmi), + fontSize = 52.sp, + fontWeight = FontWeight.ExtraBold, + color = bmiCategoryColor + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = bmiCategory, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + color = bmiCategoryColor + ) + Spacer(modifier = Modifier.height(16.dp)) + BmiIndicatorBar(category = bmiCategory) + } + } + + // --- Bagian Input Data --- + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(16.dp) // Jarak antar kartu + ) { + Text( + text = "Pengaturan & Input", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 8.dp, bottom = 8.dp), + fontWeight = FontWeight.Bold + ) + + Card(modifier = Modifier.fillMaxWidth()) { + UnitSwitchRow( + isMetric = useMetric, + onUnitChanged = { useMetric = it }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + Card(modifier = Modifier.fillMaxWidth()) { + InputWithImage( + imageRes = R.drawable.ic_height, + label = if (useMetric) R.string.height_cm else R.string.height_in, + value = heightInput, + onValueChanged = { + heightInput = it + // Reset error saat pengguna mulai mengetik lagi + isHeightError = false + }, + isError = isHeightError, + errorMessage = "Input tidak valid (1-${maxHeightCm.toInt()})", + imeAction = ImeAction.Next, + modifier = Modifier.padding(16.dp) + ) + } + + Card(modifier = Modifier.fillMaxWidth()) { + InputWithImage( + imageRes = R.drawable.ic_weight, + label = if (useMetric) R.string.weight_kg else R.string.weight_lbs, + value = weightInput, + onValueChanged = { + weightInput = it + // Reset error saat pengguna mulai mengetik lagi + isWeightError = false + }, + isError = isWeightError, + errorMessage = "Input tidak valid (1-${maxWeightKg.toInt()})", + imeAction = ImeAction.Done, + // Tambahkan keyboardActions untuk memicu perhitungan saat "Done" ditekan + keyboardActions = KeyboardActions(onDone = { performCalculation() }), + modifier = Modifier.padding(16.dp) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) // Spacer untuk mendorong tombol ke bawah + + // --- TOMBOL HITUNG BARU --- + Button( + onClick = { performCalculation() }, // Panggil fungsi perhitungan + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + .height(50.dp) + ) { + Text("HITUNG BMI", fontSize = 18.sp, fontWeight = FontWeight.Bold) + } + } +} + +// --- Composable InputWithImage diperbarui --- @Composable fun InputWithImage( @DrawableRes imageRes: Int, @@ -34,8 +255,7 @@ fun InputWithImage( isError: Boolean, errorMessage: String, imeAction: ImeAction, - maxLength: Int, // Parameter baru untuk batas maksimal karakter - keyboardActions: KeyboardActions = KeyboardActions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, // Tambah parameter ini modifier: Modifier = Modifier ) { Row( @@ -53,12 +273,7 @@ fun InputWithImage( Column(modifier = Modifier.weight(1f)) { OutlinedTextField( value = value, - onValueChange = { - // LOGIKA BARU UNTUK MEMBATASI INPUT - if (it.length <= maxLength) { - onValueChanged(it) - } - }, + onValueChange = onValueChanged, label = { Text(stringResource(label)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, @@ -67,7 +282,7 @@ fun InputWithImage( singleLine = true, modifier = Modifier.fillMaxWidth(), isError = isError, - keyboardActions = keyboardActions + keyboardActions = keyboardActions // Terapkan keyboardActions ) if (isError) { Text( @@ -80,3 +295,96 @@ fun InputWithImage( } } } + + +// --- FUNGSI-FUNGSI PEMBANTU LAINNYA --- +// (Tidak ada perubahan signifikan di sini) + +@Composable +fun BmiIndicatorBar(category: String) { + val categories = listOf("Underweight", "Normal", "Overweight", "Obese") + Row( + modifier = Modifier + .fillMaxWidth() + .height(16.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface) + .padding(2.dp) + ) { + categories.forEach { cat -> + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background( + if (cat == category) getBmiCategoryColor(cat) else Color.LightGray.copy( + alpha = 0.3f + ) + ) + ) + } + } +} + +@Composable +fun UnitSwitchRow(isMetric: Boolean, onUnitChanged: (Boolean) -> Unit, modifier: Modifier = Modifier) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("Unit", fontWeight = FontWeight.SemiBold) + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Imperial", style = MaterialTheme.typography.bodyMedium) + Switch( + checked = isMetric, + onCheckedChange = onUnitChanged, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Text("Metric", style = MaterialTheme.typography.bodyMedium) + } + } +} + +private fun calculateBMI(height: Double, weight: Double, isMetric: Boolean): Double { + if (height <= 0 || weight <= 0) return 0.0 + if (height > 250 || weight > 300) return 0.0 + + return if (isMetric) { + val heightInMeters = height / 100 + weight / (heightInMeters * heightInMeters) + } else { + 703 * weight / (height * height) + } +} + +private fun bmiCategory(bmi: Double): String { + return when { + bmi == 0.0 -> "..." + bmi < 18.5 -> "Underweight" + bmi < 25.0 -> "Normal" + bmi < 30.0 -> "Overweight" + else -> "Obese" + } +} + +@Composable +private fun getBmiCategoryColor(category: String): Color { + return when (category) { + "Underweight" -> Color(0xFF8AB4F8) + "Normal" -> Color(0xFF5BB974) + "Overweight" -> Color(0xFFFDD663) + "Obese" -> Color(0xFFE57373) + else -> MaterialTheme.colorScheme.onSurface + } +} + +@Preview(showBackground = true, name = "Final BMI Calculator with Button") +@Composable +fun BMICalculatorScreenPreview() { + TipTimeTheme { + Surface(color = MaterialTheme.colorScheme.surfaceContainerLowest) { + BMICalculatorScreen() + } + } +}