Compare commits
3 Commits
b0531c0412
...
0416151945
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0416151945 | ||
|
|
c1b2fc181b | ||
|
|
56ec2b4263 |
54
.idea/copilot.data.migration.agent.xml
generated
54
.idea/copilot.data.migration.agent.xml
generated
@ -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>
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user