Compare commits

...

3 Commits

10 changed files with 329 additions and 109 deletions

View File

@ -4,26 +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="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="a0c23b11-ba1e-4275-a3e4-38b3c8fdbe00" />
<option value="a20448f7-57cd-4a40-89b1-460451412588" />
<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>
@ -77,15 +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/java/id/ac/ubharajaya/panicbutton/MainScreen.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml" /> <option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt" />
<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" />
</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

@ -69,4 +69,7 @@ dependencies {
// AndroidX SplashScreen (required for installSplashScreen API) // AndroidX SplashScreen (required for installSplashScreen API)
implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
// Play Services Location for GPS (FusedLocationProviderClient)
implementation("com.google.android.gms:play-services-location:21.0.1")
} }

View File

@ -2,19 +2,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:allowBackup="true" android:allowBackup="true"
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" />
@ -34,5 +37,4 @@
android:label="Detail Peta" /> android:label="Detail Peta" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

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

@ -1,24 +1,68 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import com.google.android.gms.location.LocationServices
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var viewModel: MainViewModel private lateinit var viewModel: MainViewModel
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
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."
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java) viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val fusedClient = LocationServices.getFusedLocationProviderClient(this)
val openEvacMaps = { val openEvacMaps = {
startActivity(Intent(this, EvacuationMapsActivity::class.java)) startActivity(Intent(this, EvacuationMapsActivity::class.java))
} }
@SuppressLint("MissingPermission")
fun handleSendAlert() {
// Check permission
val fineGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
val coarseGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (!fineGranted && !coarseGranted) {
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
viewModel.dialogMessage = "Izin lokasi dibutuhkan untuk menyertakan koordinat. Silakan tekan lagi setelah mengizinkan lokasi."
return
}
fusedClient.lastLocation
.addOnSuccessListener { loc ->
if (loc != null) {
viewModel.sendAlert(loc.latitude, loc.longitude)
} else {
viewModel.sendAlert()
}
}
.addOnFailureListener {
viewModel.sendAlert()
}
}
setContent { setContent {
MaterialTheme { MaterialTheme {
MainScreen(viewModel = viewModel, onOpenEvacMaps = openEvacMaps) MainScreen(viewModel = viewModel, onOpenEvacMaps = openEvacMaps, onSendAlert = ::handleSendAlert)
} }
} }
} }

View File

@ -22,57 +22,59 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.foundation.layout.IntrinsicSize
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> 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 = { viewModel.sendAlert() }, 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))
@ -88,23 +90,42 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) {
} }
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
val dialogMessage = viewModel.dialogMessage viewModel.dialogMessage
if (!dialogMessage.isNullOrBlank()) { ?.takeIf { it.isNotBlank() }
AlertDialog( ?.let { msg ->
onDismissRequest = { viewModel.clearDialog() }, AlertDialog(
confirmButton = { onDismissRequest = { viewModel.clearDialog() },
TextButton(onClick = { viewModel.clearDialog() }) { Text("OK") } confirmButton = {
}, TextButton(onClick = { viewModel.clearDialog() }) { Text("OK") }
title = { Text("🚨 Notifikasi", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)) }, },
text = { Text(dialogMessage ?: "") } title = { Text("🚨 Notifikasi", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)) },
) text = { Text(msg) }
} )
}
} }
} }
// =======================================================
// EmergencyConditionCard
// =======================================================
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EmergencyConditionCard(viewModel: MainViewModel) { fun EmergencyConditionCard(viewModel: MainViewModel) {
@ -242,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)
@ -341,6 +361,10 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
} }
} }
// =======================================================
// EmergencyOptionItem
// =======================================================
@Composable @Composable
fun EmergencyOptionItem( fun EmergencyOptionItem(
option: EmergencyOption, option: EmergencyOption,
@ -362,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,
@ -420,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,12 @@ 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 = ""
} }
} }
fun sendAlert() { 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 }
if (selectedOptions.isEmpty()) { if (selectedOptions.isEmpty()) {
@ -53,15 +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()}")
} }
if (latitude != null && longitude != null) {
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>