feat: add maps evacuation

This commit is contained in:
rakha 2025-11-29 09:52:05 +07:00
parent c1b2fc181b
commit 0416151945
9 changed files with 255 additions and 106 deletions

View File

@ -4,30 +4,61 @@
<option name="migrationStatus" value="IN_PROGRESS" /> <option name="migrationStatus" value="IN_PROGRESS" />
<option name="pendingSessionIds"> <option name="pendingSessionIds">
<option value="05f02dcc-4a62-4a3b-a6bb-d4764cbd1bab" /> <option value="05f02dcc-4a62-4a3b-a6bb-d4764cbd1bab" />
<option value="1aa6c162-aaff-428f-bfae-3b53f8ce3f76" />
<option value="1e2fee64-523c-4038-ad08-03d8f8196cb1" /> <option value="1e2fee64-523c-4038-ad08-03d8f8196cb1" />
<option value="46f36349-a2e4-46bf-b85e-8ee46252660a" /> <option value="46f36349-a2e4-46bf-b85e-8ee46252660a" />
<option value="4e8befa7-c7cb-4874-9ac5-4b69e81d19b8" />
<option value="63e1cd83-39a1-4de9-83e4-a7d978cb9048" /> <option value="63e1cd83-39a1-4de9-83e4-a7d978cb9048" />
<option value="82cd9415-28a8-49b5-841d-6178af5419de" /> <option value="82cd9415-28a8-49b5-841d-6178af5419de" />
<option value="9e78818c-2d23-4dc2-8dd8-c82d3ade6f9d" />
<option value="a1f1bc1b-b1d4-4655-9677-4653b213cc14" /> <option value="a1f1bc1b-b1d4-4655-9677-4653b213cc14" />
<option value="ad5bcc95-675a-45d4-8896-b1645cb788b9" /> <option value="ad5bcc95-675a-45d4-8896-b1645cb788b9" />
<option value="b944cd09-1573-42de-840e-06d91ce8729c" />
<option value="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" /> <option value="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" />
<option value="d23e68d4-6558-4c12-a57e-d937ea639547" /> <option value="d23e68d4-6558-4c12-a57e-d937ea639547" />
<option value="dd38c49b-b13d-4206-8c24-bcf78317eec5" />
<option value="e6a32dc0-5413-49cf-ad92-9fb87982ed75" /> <option value="e6a32dc0-5413-49cf-ad92-9fb87982ed75" />
</option> </option>
<option name="pendingTurns"> <option name="pendingTurns">
<map> <map>
<entry key="1aa6c162-aaff-428f-bfae-3b53f8ce3f76">
<value>
<set>
<option value="cbcc2ea0-3963-436d-be88-f495313b8d20" />
<option value="c00b235a-a5a8-4158-baf4-7a3ca74345b0" />
<option value="16815d5b-8699-467d-9929-397e12de1084" />
<option value="f6f34f7d-39e2-4634-a01d-82dae28320aa" />
<option value="43c8a7ee-b78d-4cf3-824e-8d0c98692b00" />
<option value="eab6d8f0-3fdf-46bc-ab06-c8eb1243fe00" />
<option value="207107b2-1903-41f3-a6d3-30ee3505d45b" />
<option value="73b11a61-7794-4de6-bbdf-5f37c67fd8f0" />
</set>
</value>
</entry>
<entry key="63e1cd83-39a1-4de9-83e4-a7d978cb9048"> <entry key="63e1cd83-39a1-4de9-83e4-a7d978cb9048">
<value> <value>
<set> <set>
<option value="f099f72f-0866-4d16-85e4-8323be39e45f" />
<option value="44c8c4f4-3bbe-4fb8-88e4-58bdb184dad0" />
<option value="a20448f7-57cd-4a40-89b1-460451412588" />
<option value="484561a1-1c08-44cd-bcd3-a53895fc3851" />
<option value="17ec376a-38fd-48ac-a4e1-52e9f7df3100" /> <option value="17ec376a-38fd-48ac-a4e1-52e9f7df3100" />
<option value="a0c23b11-ba1e-4275-a3e4-38b3c8fdbe00" />
<option value="3ac3800e-363e-4148-86b3-0d4f3b0a8b62" /> <option value="3ac3800e-363e-4148-86b3-0d4f3b0a8b62" />
<option value="44c8c4f4-3bbe-4fb8-88e4-58bdb184dad0" />
<option value="484561a1-1c08-44cd-bcd3-a53895fc3851" />
<option value="5460190d-c8ff-4194-9316-a6298c7cb54b" /> <option value="5460190d-c8ff-4194-9316-a6298c7cb54b" />
<option value="a0c23b11-ba1e-4275-a3e4-38b3c8fdbe00" />
<option value="a20448f7-57cd-4a40-89b1-460451412588" />
<option value="c640b33a-4bc6-4ee0-98ed-6a71e7270331" /> <option value="c640b33a-4bc6-4ee0-98ed-6a71e7270331" />
<option value="f099f72f-0866-4d16-85e4-8323be39e45f" />
</set>
</value>
</entry>
<entry key="9e78818c-2d23-4dc2-8dd8-c82d3ade6f9d">
<value>
<set>
<option value="3a14eb41-53f1-4190-a7f8-24c23cb882e8" />
<option value="4a2475a2-98ab-4ae0-b689-cbc2da702b83" />
<option value="72c7c9c6-ecb5-444b-86de-559ea9d893e5" />
<option value="a58fb4e2-20bd-480e-9eff-a5b9dc57c1b4" />
<option value="f43c2e1c-5a6c-4deb-9fbf-5e7694c69f6d" />
<option value="faff10ea-f875-4de1-adc0-6095c7c3e718" />
</set> </set>
</value> </value>
</entry> </entry>
@ -81,18 +112,14 @@
</map> </map>
</option> </option>
<pendingWorkingSetItems> <pendingWorkingSetItems>
<entry key="63e1cd83-39a1-4de9-83e4-a7d978cb9048"> <entry key="1aa6c162-aaff-428f-bfae-3b53f8ce3f76">
<set> <set>
<option value="file://$PROJECT_DIR$/app/src/main/res/mipmap-anydpi/ic_launcher.xml" />
<option value="file://$PROJECT_DIR$/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml" />
<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/NotificationSender.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt" /> <option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/AndroidManifest.xml" /> <option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt" />
</set> </set>
</entry> </entry>
<entry key="63e1cd83-39a1-4de9-83e4-a7d978cb9048" />
<entry key="9e78818c-2d23-4dc2-8dd8-c82d3ade6f9d" />
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" /> <entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" />
</pendingWorkingSetItems> </pendingWorkingSetItems>
</component> </component>

View File

@ -11,14 +11,13 @@
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/logo" android:icon="@drawable/logo"
android:label="@string/app_name"
android:roundIcon="@drawable/logo" android:roundIcon="@drawable/logo"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.PanicButton"> android:theme="@style/Theme.PanicButton"
android:label="@string/app_name">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.App.Starting"> android:theme="@style/Theme.App.Starting">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -1,58 +1,170 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material.icons.Icons
import androidx.compose.material3.CardDefaults import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController 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 @Composable
fun EvacuationMapsScreen(navController: NavController, onBack: () -> Unit) { fun EvacuationMapsScreen(navController: NavController, onBack: () -> Unit) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { Scaffold(
Text(text = "Peta Jalur Evakuasi", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 12.dp)) topBar = {
TopAppBar(
// Card 1 - Selatan title = { Text("Peta Jalur Evakuasi") },
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { navController.navigate("evac_map/selatan") }, shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)) { navigationIcon = {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp)) { IconButton(onClick = onBack) {
Image(painter = painterResource(id = R.drawable.lantai_1_selatan), contentDescription = "Selatan", modifier = Modifier.size(120.dp)) Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali")
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.")
} }
} )
} }
) { 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 Spacer(modifier = Modifier.height(8.dp))
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)) { // Card 1 - Selatan
Image(painter = painterResource(id = R.drawable.lantai_1_utara), contentDescription = "Utara", modifier = Modifier.size(120.dp)) EvacuationMapCard(
Spacer(modifier = Modifier.width(12.dp)) title = "Jalur Evakuasi Selatan",
Column { description = "Lihat detail jalur evakuasi lantai selatan.",
Text(text = "Jalur Evakuasi Utara", fontSize = 18.sp) drawableId = R.drawable.lantai_1_selatan,
Spacer(modifier = Modifier.height(6.dp)) onClick = { navController.navigate("evac_map/selatan") }
Text(text = "Lihat detail jalur evakuasi lantai utara.") )
}
} // 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 @Composable
fun EvacuationMapDetailScreen(drawableResId: Int, onBack: () -> Unit) { fun EvacuationMapCard(
Column(modifier = Modifier.fillMaxSize().padding(12.dp)) { title: String,
Text(text = "Detail Peta", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 12.dp)) description: String,
Image(painter = painterResource(id = drawableResId), contentDescription = "Map Detail", modifier = Modifier.fillMaxWidth().heightIn(max = 600.dp)) 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
)
}
}
}

View File

@ -18,10 +18,7 @@ class MainActivity : ComponentActivity() {
private val requestPermissionLauncher = private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
// Inform user about permission result
if (isGranted) { 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." if (::viewModel.isInitialized) viewModel.dialogMessage = "Izin lokasi diberikan. Tekan kembali untuk menyertakan koordinat."
} else { } else {
if (::viewModel.isInitialized) viewModel.dialogMessage = "Izin lokasi ditolak. Laporan akan dikirim tanpa koordinat." 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 val coarseGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (!fineGranted && !coarseGranted) { if (!fineGranted && !coarseGranted) {
// Request fine location permission (preferred)
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) 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." viewModel.dialogMessage = "Izin lokasi dibutuhkan untuk menyertakan koordinat. Silakan tekan lagi setelah mengizinkan lokasi."
return return
} }
// Try get last location asynchronously via Task listeners
fusedClient.lastLocation fusedClient.lastLocation
.addOnSuccessListener { loc -> .addOnSuccessListener { loc ->
if (loc != null) { if (loc != null) {
viewModel.sendAlert(loc.latitude, loc.longitude) viewModel.sendAlert(loc.latitude, loc.longitude)
} else { } else {
// fallback: send without coordinates
viewModel.sendAlert() viewModel.sendAlert()
} }
} }
.addOnFailureListener { .addOnFailureListener {
// If obtaining location fails, send without coordinates
viewModel.sendAlert() viewModel.sendAlert()
} }
} }

View File

@ -29,48 +29,52 @@ import androidx.compose.ui.text.style.TextAlign
fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert: () -> Unit) { fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert: () -> Unit) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { topBar = {
// Put the emergency box in the topBar so Scaffold will position it below the status bar automatically
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(120.dp), .padding(horizontal = 20.dp, vertical = 8.dp),
contentAlignment = Alignment.TopCenter 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( Column(
modifier = Modifier modifier = contentPaddingModifier
.fillMaxSize() .verticalScroll(rememberScrollState())
.padding(padding) .padding(horizontal = 20.dp),
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally 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) EmergencyConditionCard(viewModel)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@ -86,9 +90,23 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert
} }
Spacer(modifier = Modifier.height(20.dp)) 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 viewModel.dialogMessage
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
?.let { msg -> ?.let { msg ->
@ -104,6 +122,10 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert
} }
} }
// =======================================================
// EmergencyConditionCard
// =======================================================
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EmergencyConditionCard(viewModel: MainViewModel) { fun EmergencyConditionCard(viewModel: MainViewModel) {
@ -241,7 +263,6 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false }, onDismissRequest = { expanded = false },
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.background( .background(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
@ -340,6 +361,10 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
} }
} }
// =======================================================
// EmergencyOptionItem
// =======================================================
@Composable @Composable
fun EmergencyOptionItem( fun EmergencyOptionItem(
option: EmergencyOption, option: EmergencyOption,
@ -361,7 +386,7 @@ fun EmergencyOptionItem(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = option.icon, text = option.icon,
@ -419,6 +444,10 @@ fun EmergencyOptionItem(
} }
} }
// =======================================================
// SelectedChip
// =======================================================
@Composable @Composable
fun SelectedChip(option: EmergencyOption, viewModel: MainViewModel) { fun SelectedChip(option: EmergencyOption, viewModel: MainViewModel) {
Box( Box(

View File

@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// Ganti ReportOption dengan EmergencyOption
data class EmergencyOption( data class EmergencyOption(
val label: String, val label: String,
val icon: String val icon: String
@ -34,13 +33,11 @@ class MainViewModel : ViewModel() {
fun setChecked(label: String, isChecked: Boolean) { fun setChecked(label: String, isChecked: Boolean) {
_checkedState[label] = isChecked _checkedState[label] = isChecked
// Clear otherNote jika "Lainnya" di-uncheck
if (label == "Lainnya" && !isChecked) { if (label == "Lainnya" && !isChecked) {
otherNote = "" otherNote = ""
} }
} }
// latitude/longitude optional parameters
fun sendAlert(latitude: Double? = null, longitude: Double? = null) { fun sendAlert(latitude: Double? = null, longitude: Double? = null) {
val selectedOptions = options.filter { isChecked(it.label) }.map { it.label } val selectedOptions = options.filter { isChecked(it.label) }.map { it.label }
@ -54,21 +51,18 @@ class MainViewModel : ViewModel() {
return return
} }
// Build message payload
val bodyBuilder = StringBuilder() val bodyBuilder = StringBuilder()
bodyBuilder.append("Kondisi: ${selectedOptions.joinToString(", ")}") bodyBuilder.append("Kondisi: ${selectedOptions.joinToString(", ")}")
if (otherNote.isNotBlank()) { if (otherNote.isNotBlank()) {
bodyBuilder.append("\nCatatan: ${otherNote.trim()}") bodyBuilder.append("\nCatatan: ${otherNote.trim()}")
} }
// Append coordinates if available
if (latitude != null && longitude != null) { if (latitude != null && longitude != null) {
bodyBuilder.append("\nLokasi: https://maps.google.com/?q=$latitude,$longitude") bodyBuilder.append("\nLokasi: https://maps.google.com/?q=$latitude,$longitude")
} }
val payload = bodyBuilder.toString() val payload = bodyBuilder.toString()
// Use NotificationSender utility to send the payload
viewModelScope.launch { viewModelScope.launch {
try { try {
val resultMessage = NotificationSender.sendNotification(payload) val resultMessage = NotificationSender.sendNotification(payload)

View File

@ -30,11 +30,7 @@ object NotificationSender {
try { try {
val url = "$server/$topic" val url = "$server/$topic"
Log.d(TAG, "Preparing notification to $url") Log.d(TAG, "Preparing notification to $url")
// Title ONLY has emoji
val titleSafe = sanitizeHeaderValue("🚨 Alert 🚨") val titleSafe = sanitizeHeaderValue("🚨 Alert 🚨")
// Body is clean text, no emoji auto-append
val cleanMessage = message.trim() val cleanMessage = message.trim()
val body = cleanMessage.toRequestBody("text/plain".toMediaType()) val body = cleanMessage.toRequestBody("text/plain".toMediaType())

View File

@ -2,8 +2,8 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:drawable="@drawable/ubhara" android:drawable="@drawable/ubhara"
android:top="30dp" android:top="50dp"
android:bottom="30dp" android:bottom="50dp"
android:left="30dp" android:left="50dp"
android:right="30dp"/> android:right="50dp"/>
</layer-list> </layer-list>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.App.Starting" parent="Theme.SplashScreen"> <style name="Theme.App.Starting" parent="Theme.SplashScreen.IconBackground">
<item name="postSplashScreenTheme">@style/Theme.PanicButton</item> <item name="postSplashScreenTheme">@style/Theme.PanicButton</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon_padded</item> <item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon_padded</item>