diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml
index a1ee19c..6a51422 100644
--- a/.idea/copilot.data.migration.agent.xml
+++ b/.idea/copilot.data.migration.agent.xml
@@ -4,30 +4,61 @@
+
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 013d055..6004be7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,14 +11,13 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/logo"
- android:label="@string/app_name"
android:roundIcon="@drawable/logo"
android:supportsRtl="true"
- android:theme="@style/Theme.PanicButton">
+ android:theme="@style/Theme.PanicButton"
+ android:label="@string/app_name">
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt
index 78bd56b..18b8f75 100644
--- a/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt
@@ -1,58 +1,170 @@
package id.ac.ubharajaya.panicbutton
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
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.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
+// --- Constants for better readability ---
+val CardElevation = 8.dp
+val CornerRadius = 16.dp
+val PaddingDefault = 16.dp
+val ImageSize = 100.dp
+
+// ====================================================================
+
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EvacuationMapsScreen(navController: NavController, onBack: () -> Unit) {
- Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
- Text(text = "Peta Jalur Evakuasi", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 12.dp))
-
- // Card 1 - Selatan
- Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { navController.navigate("evac_map/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.")
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Peta Jalur Evakuasi") },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali")
+ }
}
- }
+ )
}
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(horizontal = PaddingDefault)
+ ) {
+ // Deskripsi Singkat
+ Text(
+ text = "Pilih area peta jalur evakuasi yang ingin Anda lihat.",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(bottom = PaddingDefault)
+ )
- // Card 2 - Utara
- Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { navController.navigate("evac_map/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.")
- }
- }
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Card 1 - Selatan
+ EvacuationMapCard(
+ title = "Jalur Evakuasi Selatan",
+ description = "Lihat detail jalur evakuasi lantai selatan.",
+ drawableId = R.drawable.lantai_1_selatan,
+ onClick = { navController.navigate("evac_map/selatan") }
+ )
+
+ // Card 2 - Utara
+ EvacuationMapCard(
+ title = "Jalur Evakuasi Utara",
+ description = "Lihat detail jalur evakuasi lantai utara.",
+ drawableId = R.drawable.lantai_1_utara,
+ onClick = { navController.navigate("evac_map/utara") }
+ )
}
}
}
@Composable
-fun EvacuationMapDetailScreen(drawableResId: Int, onBack: () -> Unit) {
- Column(modifier = Modifier.fillMaxSize().padding(12.dp)) {
- Text(text = "Detail Peta", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 12.dp))
- Image(painter = painterResource(id = drawableResId), contentDescription = "Map Detail", modifier = Modifier.fillMaxWidth().heightIn(max = 600.dp))
+fun EvacuationMapCard(
+ title: String,
+ description: String,
+ drawableId: Int,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp)
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(CornerRadius),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = CardElevation)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(PaddingDefault)
+ ) {
+ // Gambar dengan shape dan padding
+ Image(
+ painter = painterResource(id = drawableId),
+ contentDescription = title,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(ImageSize)
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ )
+
+ Spacer(modifier = Modifier.width(PaddingDefault))
+
+ // Kolom Teks
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
}
}
+
+// ====================================================================
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EvacuationMapDetailScreen(drawableResId: Int, onBack: () -> Unit) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Detail Peta") },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali")
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // Gambar Detail Peta
+ Image(
+ painter = painterResource(id = drawableResId),
+ contentDescription = "Map Detail",
+ contentScale = ContentScale.Fit, // Gunakan Fit untuk memastikan peta terlihat keseluruhan
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(PaddingDefault)
+ .clip(RoundedCornerShape(CornerRadius))
+ .background(Color.LightGray) // Warna latar belakang untuk gambar
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt
index 3d79fb0..37b5022 100644
--- a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt
@@ -18,10 +18,7 @@ class MainActivity : ComponentActivity() {
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
- // Inform user about permission result
if (isGranted) {
- // User granted permission
- // Ask user to press the panic button again to include location
if (::viewModel.isInitialized) viewModel.dialogMessage = "Izin lokasi diberikan. Tekan kembali untuk menyertakan koordinat."
} else {
if (::viewModel.isInitialized) viewModel.dialogMessage = "Izin lokasi ditolak. Laporan akan dikirim tanpa koordinat."
@@ -45,25 +42,20 @@ class MainActivity : ComponentActivity() {
val coarseGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (!fineGranted && !coarseGranted) {
- // Request fine location permission (preferred)
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
- // After permission flow, user will need to press the button again to actually send with location
viewModel.dialogMessage = "Izin lokasi dibutuhkan untuk menyertakan koordinat. Silakan tekan lagi setelah mengizinkan lokasi."
return
}
-
- // Try get last location asynchronously via Task listeners
+
fusedClient.lastLocation
.addOnSuccessListener { loc ->
if (loc != null) {
viewModel.sendAlert(loc.latitude, loc.longitude)
} else {
- // fallback: send without coordinates
viewModel.sendAlert()
}
}
.addOnFailureListener {
- // If obtaining location fails, send without coordinates
viewModel.sendAlert()
}
}
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt
index 698ab25..c26dbb6 100644
--- a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt
@@ -29,48 +29,52 @@ import androidx.compose.ui.text.style.TextAlign
fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert: () -> Unit) {
Scaffold(
modifier = Modifier.fillMaxSize(),
- bottomBar = {
+ topBar = {
+ // Put the emergency box in the topBar so Scaffold will position it below the status bar automatically
Box(
modifier = Modifier
.fillMaxWidth()
- .height(120.dp),
+ .padding(horizontal = 20.dp, vertical = 8.dp),
contentAlignment = Alignment.TopCenter
) {
- PanicButton(onClick = { onSendAlert() }, buttonSize = 130.dp, shadowSize = 160.dp)
+ Text(
+ text = "JANGAN PANIK, SEGERA EVAKUASI DIRI ANDA KE TITIK KUMPUL!!",
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold),
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .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
+ )
}
}
- ) { padding ->
+ ) { padding -> // Inset padding yang disediakan oleh Scaffold
+
+ // Use the Scaffold padding for content so it's laid out under the topBar correctly
+ val contentPaddingModifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+
Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding)
- .padding(20.dp),
+ modifier = contentPaddingModifier
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- // Emergency instruction
- Text(
- text = "JANGAN PANIK, SEGERA EVAKUASI DIRI ANDA KE TITIK KUMPUL!!",
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodyLarge.copy(
- fontWeight = FontWeight.Bold
- ),
- textAlign = TextAlign.Center,
- modifier = Modifier
- .fillMaxWidth()
- .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,
- )
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Main card and other content
EmergencyConditionCard(viewModel)
Spacer(modifier = Modifier.height(12.dp))
@@ -86,9 +90,23 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert
}
Spacer(modifier = Modifier.height(20.dp))
+
+ // Move PanicButton into scrollable content so it scrolls with the page
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ // keep same size and shadow as before
+ PanicButton(onClick = { onSendAlert() }, buttonSize = 130.dp, shadowSize = 160.dp)
+ }
+
+ // create some extra space below button for comfortable scrolling
+ Spacer(modifier = Modifier.height(40.dp))
}
- // Dialog
+ // Dialog notifikasi
viewModel.dialogMessage
?.takeIf { it.isNotBlank() }
?.let { msg ->
@@ -104,6 +122,10 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert
}
}
+// =======================================================
+// EmergencyConditionCard
+// =======================================================
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmergencyConditionCard(viewModel: MainViewModel) {
@@ -241,7 +263,6 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
- .fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp)
@@ -340,6 +361,10 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
}
}
+// =======================================================
+// EmergencyOptionItem
+// =======================================================
+
@Composable
fun EmergencyOptionItem(
option: EmergencyOption,
@@ -361,7 +386,7 @@ fun EmergencyOptionItem(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp)
),
- contentAlignment = Alignment.Center
+ contentAlignment = Alignment.Center
) {
Text(
text = option.icon,
@@ -419,6 +444,10 @@ fun EmergencyOptionItem(
}
}
+// =======================================================
+// SelectedChip
+// =======================================================
+
@Composable
fun SelectedChip(option: EmergencyOption, viewModel: MainViewModel) {
Box(
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt
index de709a5..cf2171c 100644
--- a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt
@@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
-// Ganti ReportOption dengan EmergencyOption
data class EmergencyOption(
val label: String,
val icon: String
@@ -34,13 +33,11 @@ class MainViewModel : ViewModel() {
fun setChecked(label: String, isChecked: Boolean) {
_checkedState[label] = isChecked
- // Clear otherNote jika "Lainnya" di-uncheck
if (label == "Lainnya" && !isChecked) {
otherNote = ""
}
}
- // latitude/longitude optional parameters
fun sendAlert(latitude: Double? = null, longitude: Double? = null) {
val selectedOptions = options.filter { isChecked(it.label) }.map { it.label }
@@ -54,21 +51,18 @@ class MainViewModel : ViewModel() {
return
}
- // Build message payload
val bodyBuilder = StringBuilder()
bodyBuilder.append("Kondisi: ${selectedOptions.joinToString(", ")}")
if (otherNote.isNotBlank()) {
bodyBuilder.append("\nCatatan: ${otherNote.trim()}")
}
- // Append coordinates if available
if (latitude != null && longitude != null) {
bodyBuilder.append("\nLokasi: https://maps.google.com/?q=$latitude,$longitude")
}
val payload = bodyBuilder.toString()
- // Use NotificationSender utility to send the payload
viewModelScope.launch {
try {
val resultMessage = NotificationSender.sendNotification(payload)
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt
index a7c9e42..0850a9c 100644
--- a/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt
@@ -30,11 +30,7 @@ object NotificationSender {
try {
val url = "$server/$topic"
Log.d(TAG, "Preparing notification to $url")
-
- // Title ONLY has emoji
val titleSafe = sanitizeHeaderValue("🚨 Alert 🚨")
-
- // Body is clean text, no emoji auto-append
val cleanMessage = message.trim()
val body = cleanMessage.toRequestBody("text/plain".toMediaType())
diff --git a/app/src/main/res/drawable/splash_icon_padded.xml b/app/src/main/res/drawable/splash_icon_padded.xml
index d121266..808dea7 100644
--- a/app/src/main/res/drawable/splash_icon_padded.xml
+++ b/app/src/main/res/drawable/splash_icon_padded.xml
@@ -2,8 +2,8 @@
+ android:top="50dp"
+ android:bottom="50dp"
+ android:left="50dp"
+ android:right="50dp"/>
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index f0b034d..2f5a2a9 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,6 +1,6 @@
-