Compare commits

..

11 Commits

Author SHA1 Message Date
6120173402 first commit 2025-11-07 20:21:29 +07:00
099c35f19a Update README.md 2025-11-06 12:04:44 +07:00
0ee43c2e9a First update calculation 2025-11-06 11:34:31 +07:00
7053fa6573 First update labels 2025-11-06 11:10:35 +07:00
51c9a8e5ff First commit 2025-11-06 09:58:07 +07:00
renovate[bot]
b029b3dd10
Update all dependencies 8.7.3 to v8.8.0 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-10 05:25:39 +00:00
renovate[bot]
90545a4a4d
Update dependency gradle 8.11.1 to v8.12 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 03:18:17 +00:00
renovate[bot]
e075e6ded8
Update dependency androidx.compose:compose-bom 2024.11.00 to v2024.12.01 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 03:31:35 +00:00
Tomáš Mlynarič
7db6c366a3 Update to kotlin 2.1.0 2024-12-09 17:16:15 +01:00
Jose Alcérreca
076428c67e
Merge pull request #257 from google-developer-training/mlykotom-renovate-fix
Add branches to renovate
2024-12-09 13:44:24 +00:00
Tomáš Mlynarič
59c134aec9
Add branches to renovate 2024-12-09 14:35:04 +01:00
10 changed files with 738 additions and 192 deletions

View File

@ -2,5 +2,10 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": [
"local>android/.github:renovate-config" "local>android/.github:renovate-config"
],
"baseBranches": [
"main",
"starter",
"state"
] ]
} }

View File

@ -1,24 +1,50 @@
Tip Time - Solution Code # Kalkulator BMI
=================================
Solution code for the [Android Basics with Compose](https://developer.android.com/courses/android-basics-compose/course): Tip Time app. Aplikasi Android sederhana untuk menghitung Indeks Massa Tubuh (IMT) atau *Body Mass Index* (BMI). Aplikasi ini dibuat sebagai bagian dari tugas atau latihan pengembangan aplikasi Android.
## Deskripsi
Introduction *Body Mass Index* (BMI) atau Indeks Massa Tubuh (IMT) adalah angka yang menjadi standar penilaian untuk menentukan apakah berat badan Anda tergolong normal, kurang, berlebih, atau obesitas. [21] Perhitungan ini didasarkan pada perbandingan antara berat dan tinggi badan. [21] Aplikasi ini menyediakan antarmuka yang mudah digunakan untuk memasukkan data dan melihat hasilnya secara langsung.
------------
The Tip Time app contains various UI elements for calculating a tip,
teaching about user input, and State in Compose.
Aplikasi ini bertujuan untuk:
* Menyediakan alat praktis bagi pengguna untuk memantau berat badan.
* Membantu pengguna memahami kategori berat badan mereka (kurang, normal, berlebih).
* Menjadi proyek latihan untuk pengembangan aplikasi Android menggunakan teknologi modern.
Pre-requisites ## Fitur
--------------
* Experience with Kotlin syntax.
* How to create and run a project in Android Studio.
* **Input Data Pengguna:** Memasukkan berat badan (kg) dan tinggi badan (cm).
* **Perhitungan BMI:** Menghitung skor BMI secara otomatis berdasarkan data yang dimasukkan.
* **Tampilan Hasil:** Menampilkan skor BMI beserta kategori berat badan (contoh: Kurus, Normal, Gemuk).
* **Antarmuka Intuitif:** Desain yang simpel dan mudah digunakan.
Getting Started ## Teknologi
---------------
1. Install Android Studio, if you don't already have it. Proyek ini dibangun menggunakan:
2. Download the sample. * **Kotlin:** Bahasa pemrograman yang direkomendasikan Google untuk pengembangan Android. [8]
3. Import the sample into Android Studio. * **Jetpack Compose:** *Toolkit* modern dari Google untuk membangun UI Android secara deklaratif, yang memungkinkan pembuatan antarmuka pengguna dengan kode yang lebih ringkas dan efisien. [2, 6, 8]
4. Build and run the sample. * **Android Studio:** Lingkungan pengembangan terintegrasi (IDE) resmi untuk pengembangan aplikasi Android. [6]
* **Material Design 3:** Implementasi sistem desain Google untuk memberikan tampilan dan nuansa yang konsisten pada aplikasi. [6]
## Petunjuk Pengembangan
Petunjuk lebih detail mengenai pengembangan dan fitur yang harus diimplementasikan dapat dibaca di dokumen berikut:
[Petunjuk Pengerjaan Proyek](https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true)
## Cara Berkontribusi
Kontribusi dari Anda sangat diharapkan! Jika Anda ingin berkontribusi pada proyek ini, silakan ikuti langkah-langkah berikut:
1. **Fork** repositori ini ke akun GitHub Anda. [1]
2. **Clone** repositori yang sudah di-fork ke mesin lokal Anda. [1]
3. Buat **branch** baru untuk setiap fitur atau perbaikan yang akan Anda kerjakan (`git checkout -b nama-fitur-anda`). [4]
4. Lakukan perubahan pada kode.
5. **Commit** perubahan Anda dengan pesan yang jelas dan deskriptif (`git commit -m 'Menambahkan fitur X'`). [4]
6. **Push** perubahan ke branch Anda di repositori fork (`git push origin nama-fitur-anda`). [4]
7. Buat **Pull Request** dari branch Anda ke branch `main` repositori ini. [3]
Pastikan untuk membaca aturan kontribusi (jika ada) sebelum memulai. [1]
## Inspirasi
Proyek starter ini dimodifikasi dan terinspirasi dari codelab resmi Android:
[Basic Android Compose - Calculate Tip](https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0)

View File

@ -17,6 +17,7 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
} }
android { android {
@ -45,18 +46,15 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = JavaVersion.VERSION_17.toString()
} }
buildFeatures { buildFeatures {
compose = true compose = true
} }
composeOptions {
kotlinCompilerExtensionVersion = rootProject.extra["compose_compiler_version"].toString()
}
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@ -67,7 +65,7 @@ android {
dependencies { dependencies {
implementation(platform("androidx.compose:compose-bom:2024.11.00")) implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.activity:activity-compose:1.9.3") implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
@ -78,7 +76,7 @@ dependencies {
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.11.00")) androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1") androidTestImplementation("androidx.test.ext:junit:1.2.1")

View File

@ -1,18 +1,15 @@
/* /*
* Copyright (C) 2023 The Android Open Source Project * 🌃 Cyberpunk BMI Calculator App
* Developed with by INDRIS ALPASELA
* NPM: [202310715200]
* Nama: INDRIS ALPASELA
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0
* you may not use this file except in compliance with the License. * https://www.apache.org/licenses/LICENSE-2.0
* You may obtain a copy of the License at
* *
* https://www.apache.org/licenses/LICENSE-2.0 * Modern BMI calculator with futuristic Cyberpunk Neon theme!
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package com.example.tiptime package com.example.tiptime
import android.os.Bundle import android.os.Bundle
@ -20,177 +17,724 @@ 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.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.*
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.safeDrawingPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material.icons.Icons
import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Surface import androidx.compose.material3.*
import androidx.compose.material3.Switch import androidx.compose.runtime.*
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.tiptime.ui.theme.TipTimeTheme import com.example.tiptime.ui.theme.TipTimeTheme
import java.text.NumberFormat import kotlin.math.pow
import kotlin.math.sin
// ---------------- CYBERPUNK NEON COLORS ----------------
val NeonPink = Color(0xFFFF006E)
val NeonCyan = Color(0xFF00F5FF)
val NeonPurple = Color(0xFF8B00FF)
val NeonYellow = Color(0xFFFFFB00)
val CyberBlack = Color(0xFF0A0E27)
val CyberDarkBlue = Color(0xFF1A1F3A)
val NeonGreen = Color(0xFF39FF14)
val NeonOrange = Color(0xFFFF3C00)
// ---------------- MAIN ACTIVITY ----------------
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
TipTimeTheme { TipTimeTheme {
Surface( Surface(modifier = Modifier.fillMaxSize()) {
modifier = Modifier.fillMaxSize(), BMIApp()
) {
TipTimeLayout()
} }
} }
} }
} }
} }
// ---------------- APP NAVIGATION ----------------
@Composable @Composable
fun TipTimeLayout() { fun BMIApp() {
var amountInput by remember { mutableStateOf("") } var currentScreen by remember { mutableStateOf("home") }
var tipInput by remember { mutableStateOf("") }
var roundUp by remember { mutableStateOf(false) }
val amount = amountInput.toDoubleOrNull() ?: 0.0 Crossfade(targetState = currentScreen, label = "screen_transition") { screen ->
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0 when (screen) {
val tip = calculateTip(amount, tipPercent, roundUp) "home" -> HomeScreen(onNavigateToCalculator = { currentScreen = "calculator" })
"calculator" -> BMICalculatorScreen(onNavigateBack = { currentScreen = "home" })
}
}
}
Column( // ---------------- HOME SCREEN WITH ANIMATED NEON EFFECT ----------------
@Composable
fun HomeScreen(onNavigateToCalculator: () -> Unit) {
// Animasi untuk efek neon berkedip
val infiniteTransition = rememberInfiniteTransition(label = "neon_pulse")
val glowAlpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 0.8f,
animationSpec = infiniteRepeatable(
animation = tween(1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "glow_animation"
)
Box(
modifier = Modifier modifier = Modifier
.statusBarsPadding() .fillMaxSize()
.padding(horizontal = 40.dp) .background(
.verticalScroll(rememberScrollState()) brush = Brush.verticalGradient(
.safeDrawingPadding(), colors = listOf(CyberBlack, CyberDarkBlue, Color(0xFF0D1B2A))
horizontalAlignment = Alignment.CenterHorizontally, )
verticalArrangement = Arrangement.Center )
) { ) {
Text( // Grid lines untuk efek cyberpunk
text = stringResource(R.string.calculate_tip), Canvas(modifier = Modifier.fillMaxSize()) {
val gridSize = 60f
// Vertical lines
var x = 0f
while (x < size.width) {
drawLine(
color = NeonCyan.copy(alpha = 0.1f),
start = Offset(x, 0f),
end = Offset(x, size.height),
strokeWidth = 1f
)
x += gridSize
}
// Horizontal lines
var y = 0f
while (y < size.height) {
drawLine(
color = NeonPink.copy(alpha = 0.1f),
start = Offset(0f, y),
end = Offset(size.width, y),
strokeWidth = 1f
)
y += gridSize
}
// Neon circles dengan efek glow
drawCircle(
color = NeonPink.copy(alpha = glowAlpha * 0.2f),
radius = 200f,
center = Offset(x = size.width * 0.2f, y = size.height * 0.3f)
)
drawCircle(
color = NeonCyan.copy(alpha = glowAlpha * 0.15f),
radius = 250f,
center = Offset(x = size.width * 0.8f, y = size.height * 0.6f)
)
drawCircle(
color = NeonPurple.copy(alpha = glowAlpha * 0.18f),
radius = 180f,
center = Offset(x = size.width * 0.5f, y = size.height * 0.8f)
)
}
Column(
modifier = Modifier modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp) .fillMaxSize()
.align(alignment = Alignment.Start) .statusBarsPadding()
) .padding(32.dp)
EditNumberField( .safeDrawingPadding(),
label = R.string.bill_amount, horizontalAlignment = Alignment.CenterHorizontally,
leadingIcon = R.drawable.money, verticalArrangement = Arrangement.Center
keyboardOptions = KeyboardOptions.Default.copy( ) {
keyboardType = KeyboardType.Number, // Logo/Icon section dengan border neon
imeAction = ImeAction.Next Box(
), modifier = Modifier
value = amountInput, .size(80.dp)
onValueChanged = { amountInput = it }, .background(
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), brush = Brush.linearGradient(
) colors = listOf(NeonPink, NeonCyan)
EditNumberField( ),
label = R.string.how_was_the_service, shape = RoundedCornerShape(16.dp)
leadingIcon = R.drawable.percent, )
keyboardOptions = KeyboardOptions.Default.copy( .padding(2.dp)
keyboardType = KeyboardType.Number, .background(CyberBlack, RoundedCornerShape(14.dp)),
imeAction = ImeAction.Done contentAlignment = Alignment.Center
), ) {
value = tipInput, Text(
onValueChanged = { tipInput = it }, text = "",
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), fontSize = 40.sp,
) color = NeonCyan
RoundTheTipRow( )
roundUp = roundUp, }
onRoundUpChanged = { roundUp = it },
modifier = Modifier.padding(bottom = 32.dp) Spacer(Modifier.height(24.dp))
)
Text( Text(
text = stringResource(R.string.tip_amount, tip), text = "CYBER BMI",
style = MaterialTheme.typography.displaySmall style = MaterialTheme.typography.displayLarge.copy(
) fontWeight = FontWeight.Black,
Spacer(modifier = Modifier.height(150.dp)) brush = Brush.linearGradient(
colors = listOf(NeonPink, NeonCyan, NeonPurple)
)
),
textAlign = TextAlign.Center
)
Text(
text = "// CALCULATOR_V2.0",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Light,
letterSpacing = 4.sp
),
color = NeonCyan.copy(alpha = 0.7f),
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 8.dp)
)
Text(
text = "< Developed by Faris Naufal />",
style = MaterialTheme.typography.bodySmall.copy(
fontWeight = FontWeight.Light
),
color = Color.White.copy(alpha = 0.5f),
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 4.dp)
)
Spacer(Modifier.height(16.dp))
// Deskripsi dengan border cyberpunk
Card(
colors = CardDefaults.cardColors(
containerColor = CyberDarkBlue.copy(alpha = 0.6f)
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = "Hitung Body Mass Index Anda dengan teknologi futuristik",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = Color.White.copy(alpha = 0.9f),
modifier = Modifier.padding(16.dp)
)
}
Spacer(Modifier.height(48.dp))
// Button dengan efek neon gradient
Button(
onClick = onNavigateToCalculator,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp),
shape = RoundedCornerShape(12.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.horizontalGradient(
colors = listOf(NeonPink, NeonPurple, NeonCyan)
)
),
contentAlignment = Alignment.Center
) {
Text(
"⚡ MULAI SCANNING",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
),
color = Color.White
)
}
}
}
}
}
// ---------------- BMI CALCULATOR SCREEN ----------------
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BMICalculatorScreen(onNavigateBack: () -> Unit) {
var heightInput by remember { mutableStateOf("") }
var weightInput by remember { mutableStateOf("") }
var useUSUnits by remember { mutableStateOf(false) }
// Validasi input untuk mencegah nilai yang tidak wajar
val height = heightInput.toDoubleOrNull() ?: 0.0
val weight = weightInput.toDoubleOrNull() ?: 0.0
// Validasi range yang wajar
val isValidHeight = if (useUSUnits) height in 20.0..96.0 else height in 50.0..250.0
val isValidWeight = if (useUSUnits) weight in 20.0..1000.0 else weight in 10.0..500.0
val bmi = calculateBMI(height, weight, useUSUnits)
val category = calculateBMICategory(height, weight, useUSUnits)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"⚡ CYBER BMI SCANNER",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp
)
)
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Kembali",
tint = NeonCyan
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = CyberBlack,
titleContentColor = NeonPink
)
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(CyberBlack, CyberDarkBlue)
)
)
.padding(innerPadding)
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
// Unit Switch dengan desain cyberpunk
UnitSwitchRow(
useUSUnits = useUSUnits,
onUnitChanged = { useUSUnits = it },
modifier = Modifier.padding(bottom = 24.dp)
)
// Input fields dengan neon border
CyberTextField(
label = if (useUSUnits) "HEIGHT (inches) ⚡" else "TINGGI (cm) ⚡",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
value = heightInput,
onValueChanged = { heightInput = it },
isError = height > 0 && !isValidHeight,
errorMessage = if (useUSUnits) "Range: 20-96 inches" else "Range: 50-250 cm",
modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
)
CyberTextField(
label = if (useUSUnits) "WEIGHT (lbs) ⚡" else "BERAT (kg) ⚡",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
value = weightInput,
onValueChanged = { weightInput = it },
isError = weight > 0 && !isValidWeight,
errorMessage = if (useUSUnits) "Range: 20-1000 lbs" else "Range: 10-500 kg",
modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp)
)
// Tampilkan hasil hanya jika input valid
if (height > 0 && weight > 0) {
if (isValidHeight && isValidWeight) {
// Card hasil BMI dengan efek neon
Card(
colors = CardDefaults.cardColors(
containerColor = CyberDarkBlue.copy(alpha = 0.8f)
),
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
NeonPink.copy(alpha = 0.3f),
NeonCyan.copy(alpha = 0.3f)
)
),
shape = RoundedCornerShape(20.dp)
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "// SCAN RESULT",
style = MaterialTheme.typography.titleSmall.copy(
letterSpacing = 3.sp
),
color = NeonCyan.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = bmi,
style = MaterialTheme.typography.displayLarge.copy(
fontWeight = FontWeight.Black,
brush = Brush.linearGradient(
colors = listOf(
getBMICategoryColor(category),
getBMICategoryColor(category).copy(alpha = 0.7f)
)
)
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = category.uppercase(),
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
),
color = getBMICategoryColor(category)
)
}
}
Spacer(modifier = Modifier.height(24.dp))
BMICategoryTable()
} else {
// Peringatan untuk input tidak valid
Card(
colors = CardDefaults.cardColors(
containerColor = NeonOrange.copy(alpha = 0.2f)
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "⚠ INPUT TIDAK VALID\nMasukkan nilai yang wajar",
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.SemiBold
),
color = NeonOrange,
textAlign = TextAlign.Center,
modifier = Modifier.padding(16.dp)
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
// ---------------- BMI CATEGORY TABLE WITH CYBERPUNK STYLE ----------------
@Composable
fun BMICategoryTable() {
Card(
colors = CardDefaults.cardColors(
containerColor = CyberDarkBlue.copy(alpha = 0.7f)
),
shape = RoundedCornerShape(16.dp),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "// BMI CATEGORIES",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
),
color = NeonCyan,
modifier = Modifier.padding(bottom = 12.dp)
)
Divider(
color = NeonPink.copy(alpha = 0.5f),
thickness = 2.dp,
modifier = Modifier.padding(bottom = 12.dp)
)
BMICategoryRow("UNDERWEIGHT", "< 18.5", Color(0xFF00D9FF))
Spacer(modifier = Modifier.height(8.dp))
BMICategoryRow("NORMAL", "18.5 24.9", NeonGreen)
Spacer(modifier = Modifier.height(8.dp))
BMICategoryRow("OVERWEIGHT", "25 29.9", NeonYellow)
Spacer(modifier = Modifier.height(8.dp))
BMICategoryRow("OBESE", "≥ 30", NeonOrange)
}
} }
} }
@Composable @Composable
fun EditNumberField( fun BMICategoryRow(label: String, range: String, color: Color) {
@StringRes label: Int, Row(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(
colors = listOf(
color.copy(alpha = 0.3f),
color.copy(alpha = 0.1f)
)
),
shape = RoundedCornerShape(8.dp)
)
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
color = color,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = range,
color = color,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
}
}
// ---------------- CUSTOM CYBERPUNK TEXT FIELD ----------------
@Composable
fun CyberTextField(
label: String,
@DrawableRes leadingIcon: Int, @DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions, keyboardOptions: KeyboardOptions,
value: String, value: String,
onValueChanged: (String) -> Unit, onValueChanged: (String) -> Unit,
isError: Boolean = false,
errorMessage: String = "",
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
TextField( Column(modifier = modifier) {
value = value, TextField(
singleLine = true, value = value,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) }, onValueChange = onValueChanged,
modifier = modifier, singleLine = true,
onValueChange = onValueChanged, leadingIcon = {
label = { Text(stringResource(label)) }, Icon(
keyboardOptions = keyboardOptions painterResource(id = leadingIcon),
) contentDescription = null,
tint = if (isError) NeonOrange else NeonCyan
)
},
label = {
Text(
label,
color = if (isError) NeonOrange else NeonPink,
style = MaterialTheme.typography.bodyMedium.copy(
letterSpacing = 1.sp
)
)
},
placeholder = {
Text(
"Enter value...",
color = Color.Gray.copy(alpha = 0.5f)
)
},
keyboardOptions = keyboardOptions,
isError = isError,
colors = TextFieldDefaults.colors(
focusedContainerColor = CyberDarkBlue.copy(alpha = 0.6f),
unfocusedContainerColor = CyberDarkBlue.copy(alpha = 0.4f),
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedIndicatorColor = if (isError) NeonOrange else NeonCyan,
unfocusedIndicatorColor = if (isError) NeonOrange.copy(alpha = 0.5f) else NeonPurple.copy(alpha = 0.5f),
errorIndicatorColor = NeonOrange,
errorContainerColor = CyberDarkBlue.copy(alpha = 0.6f),
cursorColor = NeonCyan
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
if (isError && errorMessage.isNotEmpty()) {
Text(
text = errorMessage,
color = NeonOrange,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
}
} }
// ---------------- UNIT SWITCH WITH CYBERPUNK STYLE ----------------
@Composable @Composable
fun RoundTheTipRow( fun UnitSwitchRow(
roundUp: Boolean, useUSUnits: Boolean,
onRoundUpChanged: (Boolean) -> Unit, onUnitChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Card(
modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(
verticalAlignment = Alignment.CenterVertically containerColor = CyberDarkBlue.copy(alpha = 0.6f)
),
shape = RoundedCornerShape(12.dp),
modifier = modifier.fillMaxWidth()
) { ) {
Text(text = stringResource(R.string.round_up_tip)) Row(
Switch(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentWidth(Alignment.End), .padding(16.dp),
checked = roundUp, verticalAlignment = Alignment.CenterVertically,
onCheckedChange = onRoundUpChanged horizontalArrangement = Arrangement.SpaceBetween
) ) {
Column {
Text(
text = if (useUSUnits) "US UNITS" else "METRIC UNITS",
color = NeonCyan,
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp
)
)
Text(
text = if (useUSUnits) "inches, lbs" else "cm, kg",
color = Color.White.copy(alpha = 0.6f),
style = MaterialTheme.typography.bodySmall
)
}
Switch(
checked = useUSUnits,
onCheckedChange = onUnitChanged,
colors = SwitchDefaults.colors(
checkedThumbColor = NeonCyan,
checkedTrackColor = NeonPink.copy(alpha = 0.5f),
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = CyberBlack
)
)
}
} }
} }
// ---------------- LOGIC & UTILITIES ----------------
/**
* Menentukan warna kategori BMI berdasarkan status kesehatan
* @param category String kategori BMI
* @return Color warna yang sesuai dengan kategori
*/
@Composable
fun getBMICategoryColor(category: String): Color = when {
category.contains("Underweight", true) || category.contains("Kurus", true) -> Color(0xFF00D9FF)
category.contains("Normal", true) -> NeonGreen
category.contains("Overweight", true) || category.contains("Gemuk", true) -> NeonYellow
category.contains("Obese", true) || category.contains("Obesitas", true) -> NeonOrange
else -> Color.White
}
/** /**
* Calculates the tip based on the user input and format the tip amount * Menghitung nilai BMI berdasarkan tinggi dan berat
* according to the local currency. * Formula Metrik: BMI = weight (kg) / (height (m))^2
* Example would be "$10.00". * Formula US: BMI = (weight (lbs) / (height (inches))^2) * 703
*
* @param height Tinggi badan (cm untuk metrik, inches untuk US)
* @param weight Berat badan (kg untuk metrik, lbs untuk US)
* @param useUSUnits Boolean untuk menentukan sistem satuan
* @return String nilai BMI dengan 1 desimal
*/ */
private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String { private fun calculateBMI(height: Double, weight: Double, useUSUnits: Boolean): String {
var tip = tipPercent / 100 * amount // Validasi input tidak boleh nol atau negatif
if (roundUp) { if (height <= 0 || weight <= 0) return "-"
tip = kotlin.math.ceil(tip)
val bmi = if (useUSUnits) {
// Formula US: (weight / height^2) * 703
(weight / height.pow(2)) * 703
} else {
// Formula Metrik: weight / (height in meters)^2
weight / (height / 100).pow(2)
} }
return NumberFormat.getCurrencyInstance().format(tip)
// Format dengan 1 angka desimal
return String.format("%.1f", bmi)
}
/**
* Menentukan kategori BMI berdasarkan standar WHO
* - Underweight: BMI < 18.5
* - Normal: BMI 18.5-24.9
* - Overweight: BMI 25-29.9
* - Obese: BMI >= 30
*
* @param height Tinggi badan
* @param weight Berat badan
* @param useUSUnits Boolean sistem satuan
* @return String kategori BMI
*/
private fun calculateBMICategory(height: Double, weight: Double, useUSUnits: Boolean): String {
if (height <= 0 || weight <= 0) return "-"
val bmi = if (useUSUnits) {
(weight / height.pow(2)) * 703
} else {
weight / (height / 100).pow(2)
}
return when {
bmi < 18.5 -> "Underweight (Kurus)"
bmi < 25 -> "Normal"
bmi < 30 -> "Overweight (Gemuk)"
else -> "Obese (Obesitas)"
}
}
// ---------------- PREVIEWS ----------------
@Preview(showBackground = true)
@Composable
fun HomeScreenPreview() {
TipTimeTheme { HomeScreen(onNavigateToCalculator = {}) }
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun TipTimeLayoutPreview() { fun BMICalculatorScreenPreview() {
TipTimeTheme { TipTimeTheme { BMICalculatorScreen(onNavigateBack = {}) }
TipTimeLayout()
}
} }

View File

@ -1,23 +0,0 @@
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7.5,11C9.43,11 11,9.43 11,7.5S9.43,4 7.5,4S4,5.57 4,7.5S5.57,11 7.5,11zM7.5,6C8.33,6 9,6.67 9,7.5S8.33,9 7.5,9S6,8.33 6,7.5S6.67,6 7.5,6z"/>
<path android:fillColor="@android:color/white" android:pathData="M4.0025,18.5831l14.5875,-14.5875l1.4142,1.4142l-14.5875,14.5875z"/>
<path android:fillColor="@android:color/white" android:pathData="M16.5,13c-1.93,0 -3.5,1.57 -3.5,3.5s1.57,3.5 3.5,3.5s3.5,-1.57 3.5,-3.5S18.43,13 16.5,13zM16.5,18c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5S17.33,18 16.5,18z"/>
</vector>

View File

@ -15,10 +15,11 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<resources> <resources>
<string name="app_name">Tip Time</string> <string name="app_name">BMI Calculator</string>
<string name="calculate_tip">Calculate Tip</string> <string name="calculate_tip">Calculate BMI</string>
<string name="bill_amount">Bill Amount</string> <string name="height">Tinggi Badan</string>
<string name="how_was_the_service">Tip Percentage</string> <string name="weight">Berat Badan</string>
<string name="round_up_tip">Round up tip?</string> <string name="use_usc">Gunakan Unit USC (lbs/in)?</string>
<string name="tip_amount">Tip Amount: %s</string> <string name="bmi_calculation">BMI Anda: %s</string>
<string name="bmi_category">Kategori: %s</string>
</resources> </resources>

View File

@ -14,14 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
buildscript {
extra.apply {
set("compose_compiler_version", "1.5.3")
}
}
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.7.3" apply false id("com.android.application") version "8.13.0" apply false
id("com.android.library") version "8.7.3" apply false id("com.android.library") version "8.13.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.10" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
} }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum