From 04161519453605070d59848faca966129dfa7e07 Mon Sep 17 00:00:00 2001 From: rakha Date: Sat, 29 Nov 2025 09:52:05 +0700 Subject: [PATCH] feat: add maps evacuation --- .idea/copilot.data.migration.agent.xml | 53 ++++-- app/src/main/AndroidManifest.xml | 5 +- .../ubharajaya/panicbutton/EvacuationMaps.kt | 176 ++++++++++++++---- .../ac/ubharajaya/panicbutton/MainActivity.kt | 10 +- .../ac/ubharajaya/panicbutton/MainScreen.kt | 97 ++++++---- .../ubharajaya/panicbutton/MainViewModel.kt | 6 - .../panicbutton/NotificationSender.kt | 4 - .../main/res/drawable/splash_icon_padded.xml | 8 +- app/src/main/res/values/styles.xml | 2 +- 9 files changed, 255 insertions(+), 106 deletions(-) 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 @@ -