fix(ui): resolve dropdown 'val cannot be reassigned' and scroll list

This commit is contained in:
Rakha adi 2025-11-20 21:56:35 +07:00
parent 9280ce0a89
commit 961ebfd757
7 changed files with 181 additions and 88 deletions

View File

@ -10,9 +10,11 @@
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89">
<value>
<set>
<option value="056c3bd2-6151-4564-b839-d7d6f53d4342" />
<option value="0c019b23-843c-4710-8469-2dd1211d89a3" />
<option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" />
<option value="1bb82ec0-7888-48c9-b016-116821338f0e" />
<option value="26bdd760-c8b3-419a-9b6b-a194651aed3d" />
<option value="2a8432cf-e7cb-4250-9c7c-00664ec22a28" />
<option value="2b41e5bf-4d90-46d8-8fc8-7b41c3f788e1" />
<option value="34b3c4dc-eb50-43b4-98d0-2b9771161619" />
@ -22,11 +24,14 @@
<option value="44d2412a-3471-4972-8b90-5cad4c5a00e3" />
<option value="461c1e05-de5d-4220-af67-2f29e576b9ea" />
<option value="4f53b359-f3df-449a-ab3e-d112d5df446a" />
<option value="51c4a9e4-545f-4c67-8fee-80b3de88d2be" />
<option value="520750f7-1625-4419-beb3-c4ecf7d9dc2b" />
<option value="52e099a9-0cd7-4716-8310-8a43635ca894" />
<option value="6971fc05-6110-4b33-ac58-69628d336a48" />
<option value="6dffb37e-ecb0-4c89-8cbe-e38ed347c397" />
<option value="7216273c-1271-4ea2-88d8-177e139316f1" />
<option value="7237b251-2008-431b-b6b3-005dea48c3bf" />
<option value="72c65d6c-d369-4e6e-ad89-52749cab36a6" />
<option value="76858669-1b07-4bac-bc00-8448c673d06d" />
<option value="80ef4e51-2808-48f4-9166-bf883578c6f0" />
<option value="8589889d-a532-45c6-9ffe-0671c2d32583" />
@ -36,12 +41,14 @@
<option value="9523ff40-4e2b-4c96-b52e-69093f946698" />
<option value="9ebc3bf3-3f18-425d-a4fb-c87a6a121c9a" />
<option value="aaf8bf1e-ce4a-48ab-a65e-6da5790fc566" />
<option value="ac68227b-a347-492a-8990-f2313a4c4838" />
<option value="b32841ec-424b-46e4-af25-b31a5ca4ae51" />
<option value="ba4f0602-d62b-4a9c-b86f-ee0dce40374d" />
<option value="c759a8ff-cf83-4618-90d4-31101dea7c77" />
<option value="e2907e39-fda1-4832-99ff-fb7407d64db3" />
<option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" />
<option value="ec11cbcb-475d-4b28-8eb3-f71715723d6c" />
<option value="ee1de16e-76b5-4384-8ed5-b2e3f4e3c2cb" />
<option value="f607f271-4885-4c4e-91da-e2caf2abd67a" />
<option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" />
</set>
@ -57,6 +64,9 @@
<option value="file://$PROJECT_DIR$/app/build.gradle.kts" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMapsActivity.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMapDetailActivity.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/AndroidManifest.xml" />
</set>
</entry>
</pendingWorkingSetItems>

View File

@ -50,6 +50,8 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
// Foundation (for verticalScroll, rememberScrollState, etc.)
implementation("androidx.compose.foundation:foundation:1.5.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -22,6 +22,18 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Evacuation map activities -->
<activity
android:name=".EvacuationMapsActivity"
android:exported="false"
android:label="Peta Evakuasi" />
<activity
android:name=".EvacuationMapDetailActivity"
android:exported="false"
android:label="Detail Peta" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -0,0 +1,34 @@
package id.ac.ubharajaya.panicbutton
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
class EvacuationMapDetailActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val which = intent.getStringExtra("which") ?: "selatan"
val resId = if (which == "selatan") R.drawable.lantai_1_selatan else R.drawable.lantai_1_utara
setContent {
MaterialTheme {
EvacuationMapDetailContent(resId)
}
}
}
}
@Composable
fun EvacuationMapDetailContent(drawableResId: Int) {
Column(modifier = Modifier.fillMaxSize().padding(12.dp)) {
Text(text = "Detail Peta", modifier = Modifier.padding(bottom = 12.dp))
Image(painter = painterResource(id = drawableResId), contentDescription = "Map Detail", modifier = Modifier.fillMaxWidth().heightIn(max = 800.dp))
}
}

View File

@ -0,0 +1,68 @@
package id.ac.ubharajaya.panicbutton
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class EvacuationMapsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
EvacuationMapsContent(onSelect = { which ->
val intent = Intent(this, EvacuationMapDetailActivity::class.java)
intent.putExtra("which", which)
startActivity(intent)
})
}
}
}
}
@Composable
fun EvacuationMapsContent(onSelect: (String) -> Unit) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text(text = "Peta Jalur Evakuasi", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 12.dp))
// Selatan
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { onSelect("selatan") }, shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp)) {
Image(painter = painterResource(id = R.drawable.lantai_1_selatan), contentDescription = "Selatan", modifier = Modifier.size(120.dp))
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(text = "Jalur Evakuasi Selatan", fontSize = 18.sp)
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Lihat detail jalur evakuasi lantai selatan.")
}
}
}
// Utara
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { onSelect("utara") }, shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp)) {
Image(painter = painterResource(id = R.drawable.lantai_1_utara), contentDescription = "Utara", modifier = Modifier.size(120.dp))
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(text = "Jalur Evakuasi Utara", fontSize = 18.sp)
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Lihat detail jalur evakuasi lantai utara.")
}
}
}
}
}

View File

@ -1,51 +1,26 @@
package id.ac.ubharajaya.panicbutton
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navArgument
import androidx.navigation.compose.rememberNavController
import id.ac.ubharajaya.panicbutton.ui.theme.PanicButtonTheme
import androidx.lifecycle.ViewModelProvider
import androidx.compose.material3.MaterialTheme
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PanicButtonTheme {
AppNav(viewModel = viewModel)
}
}
}
}
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
@Composable
fun AppNav(viewModel: MainViewModel) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") {
MainScreen(viewModel = viewModel, onOpenEvacMaps = { navController.navigate("evac_maps") })
val openEvacMaps = {
startActivity(Intent(this, EvacuationMapsActivity::class.java))
}
composable("evac_maps") {
EvacuationMapsScreen(navController = navController, onBack = { navController.popBackStack() })
}
composable(
route = "evac_map/{resName}",
arguments = listOf(navArgument("resName") { type = NavType.StringType })
) { backStackEntry ->
val resName = backStackEntry.arguments?.getString("resName") ?: ""
val resId = when (resName) {
"selatan" -> R.drawable.lantai_1_selatan
"utara" -> R.drawable.lantai_1_utara
else -> 0
setContent {
MaterialTheme {
MainScreen(viewModel = viewModel, onOpenEvacMaps = openEvacMaps)
}
EvacuationMapDetailScreen(drawableResId = resId, onBack = { navController.popBackStack() })
}
}
}

View File

@ -1,37 +1,37 @@
package id.ac.ubharajaya.panicbutton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.layout.IntrinsicSize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.foundation.layout.IntrinsicSize
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) {
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
// wrap PanicButton in a Box so we can center it over the bottom bar
Box(
modifier = Modifier
.fillMaxWidth()
@ -41,73 +41,65 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) {
PanicButton(onClick = { viewModel.sendAlert() }, buttonSize = 130.dp, shadowSize = 160.dp)
}
}
) { paddingValues ->
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(padding)
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Emergency instruction text (darurat)
// Emergency instruction
Text(
text = "JANGAN PANIK, SEGERA EVAKUASI DIRI ANDA KE TITIK KUMPUL",
text = "JANGAN PANIK, SEGERA EVAKUASI DIRI ANDA KE TITIK KUMPUL!!",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold),
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Bold
),
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
.padding(bottom = 12.dp)
.background(
MaterialTheme.colorScheme.error.copy(alpha = 0.2f),
shape = RoundedCornerShape(8.dp)
)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.error,
shape = RoundedCornerShape(8.dp)
)
.padding(12.dp),
maxLines = 2,
)
// Styled Emergency Condition Card
EmergencyConditionCard(viewModel)
Spacer(Modifier.height(12.dp))
Spacer(modifier = Modifier.height(12.dp))
// Button to open evacuation maps
Button(
onClick = onOpenEvacMaps,
modifier = Modifier
.fillMaxWidth(0.6f)
.height(48.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
shape = RoundedCornerShape(12.dp)
) {
Text(text = "Lihat Peta Evakuasi", color = MaterialTheme.colorScheme.onPrimary)
Text(text = "Lihat Peta Evakuasi")
}
// Tambahkan spacer di luar Card agar ada jarak dari bottom bar
Spacer(Modifier.height(20.dp))
Spacer(modifier = Modifier.height(20.dp))
}
// Dialog Feedback
// Dialog
val dialogMessage = viewModel.dialogMessage
if (!dialogMessage.isNullOrBlank()) {
AlertDialog(
onDismissRequest = { viewModel.clearDialog() },
confirmButton = {
TextButton(onClick = { viewModel.clearDialog() }) {
Text("OK")
}
TextButton(onClick = { viewModel.clearDialog() }) { Text("OK") }
},
title = {
Text(
"Notifikasi",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold
)
)
},
text = {
Text(
dialogMessage,
style = MaterialTheme.typography.bodyMedium
)
},
containerColor = MaterialTheme.colorScheme.surface
title = { Text("🚨 Notifikasi", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)) },
text = { Text(dialogMessage ?: "") }
)
}
}
@ -231,8 +223,8 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
)
// Show first 2 selected items as chips
selectedItems.take(2).forEach { option ->
SelectedChip(option = option, viewModel = viewModel)
selectedItems.take(2).forEach { opt ->
SelectedChip(option = opt, viewModel = viewModel)
}
if (selectedItems.size > 2) {
@ -250,7 +242,7 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(IntrinsicSize.Max)
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp)
@ -263,12 +255,12 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
.heightIn(max = 280.dp)
.verticalScroll(rememberScrollState())
) {
viewModel.options.forEach { option ->
viewModel.options.forEach { opt ->
EmergencyOptionItem(
option = option,
isChecked = viewModel.isChecked(option.label),
option = opt,
isChecked = viewModel.isChecked(opt.label),
onCheckedChange = { checked ->
viewModel.setChecked(option.label, checked)
viewModel.setChecked(opt.label, checked)
}
)
}