Compare commits

...

14 Commits

Author SHA1 Message Date
rakha
0416151945 feat: add maps evacuation 2025-11-29 09:52:05 +07:00
rakha
c1b2fc181b feat: add gps 2025-11-27 18:36:09 +07:00
rakha
56ec2b4263 feat: sertakan koordinat GPS pada alert; tambahkan permission lokasi dan FusedLocationProvider; pisahkan server NTFY dan topic; perbarui MainActivity, MainViewModel, NotificationSender, MainScreen 2025-11-27 13:46:23 +07:00
rakha
b0531c0412 chore(ntfy): split NTFY server and topic; update NotificationSender and resources 2025-11-27 13:24:17 +07:00
Rakha adi
961ebfd757 fix(ui): resolve dropdown 'val cannot be reassigned' and scroll list 2025-11-20 21:56:35 +07:00
Rakha adi
9280ce0a89 feat(ui): add evacuation maps screen and navigation; add evac button to main screen 2025-11-20 20:51:40 +07:00
Rakha adi
67ae1e6643 ci: fix issues ndas ku ngelu 2025-11-20 12:19:27 +07:00
Rakha adi
04315b8e58 fix: compilation error in MainScreen (import & TextField) 2025-11-19 23:08:08 +07:00
Rakha adi
b3c44df833 feat(ui): split MainActivity into ViewModel/UI/network modules; add checklist UI & 3D PanicButton
Split MainActivity into MainViewModel, MainScreen, PanicButton, NotificationSender, and ReportOption.\n\nReplaced dropdown with checklist, added emoji icons, notes field, and validation (catatan wajib when 'Lainnya' selected). Improves maintainability and testability. Ran editor static checks; no errors reported.
2025-11-19 22:10:32 +07:00
Rakha adi
f0d6074b55 feat(ui): checklist report types with emoji icons; improved card styling and 3D panic button 2025-11-19 21:30:38 +07:00
Rakha adi
97d37a43ad feat(ui): add checklist report types and 3D panic button; include notes/validation and formatted notification 2025-11-19 21:12:45 +07:00
Rakha adi
ba36f0447b Feat(UI): Style panic button 2025-11-19 20:46:01 +07:00
nuryuda
9d34345d24 update allert tio 2025-11-13 15:54:56 +07:00
nuryuda
2e35f8bcd6 update allert yuda 2025-11-13 15:50:59 +07:00
29 changed files with 1425 additions and 89 deletions

123
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

126
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="IN_PROGRESS" />
<option name="pendingSessionIds">
<option value="05f02dcc-4a62-4a3b-a6bb-d4764cbd1bab" />
<option value="1aa6c162-aaff-428f-bfae-3b53f8ce3f76" />
<option value="1e2fee64-523c-4038-ad08-03d8f8196cb1" />
<option value="46f36349-a2e4-46bf-b85e-8ee46252660a" />
<option value="4e8befa7-c7cb-4874-9ac5-4b69e81d19b8" />
<option value="63e1cd83-39a1-4de9-83e4-a7d978cb9048" />
<option value="82cd9415-28a8-49b5-841d-6178af5419de" />
<option value="9e78818c-2d23-4dc2-8dd8-c82d3ade6f9d" />
<option value="a1f1bc1b-b1d4-4655-9677-4653b213cc14" />
<option value="ad5bcc95-675a-45d4-8896-b1645cb788b9" />
<option value="b944cd09-1573-42de-840e-06d91ce8729c" />
<option value="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" />
<option value="d23e68d4-6558-4c12-a57e-d937ea639547" />
<option value="dd38c49b-b13d-4206-8c24-bcf78317eec5" />
<option value="e6a32dc0-5413-49cf-ad92-9fb87982ed75" />
</option>
<option name="pendingTurns">
<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">
<value>
<set>
<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>
</value>
</entry>
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89">
<value>
<set>
<option value="056c3bd2-6151-4564-b839-d7d6f53d4342" />
<option value="0c019b23-843c-4710-8469-2dd1211d89a3" />
<option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" />
<option value="1bb82ec0-7888-48c9-b016-116821338f0e" />
<option value="26bdd760-c8b3-419a-9b6b-a194651aed3d" />
<option value="2a8432cf-e7cb-4250-9c7c-00664ec22a28" />
<option value="2b41e5bf-4d90-46d8-8fc8-7b41c3f788e1" />
<option value="34b3c4dc-eb50-43b4-98d0-2b9771161619" />
<option value="38d23208-212e-48d5-84ea-03d6cf590d51" />
<option value="39a0d9c4-a68f-4050-bb14-eda17dc695db" />
<option value="44cb5a83-abdc-41a2-95fa-283ec339fd2d" />
<option value="44d2412a-3471-4972-8b90-5cad4c5a00e3" />
<option value="461c1e05-de5d-4220-af67-2f29e576b9ea" />
<option value="4f53b359-f3df-449a-ab3e-d112d5df446a" />
<option value="51c4a9e4-545f-4c67-8fee-80b3de88d2be" />
<option value="520750f7-1625-4419-beb3-c4ecf7d9dc2b" />
<option value="52e099a9-0cd7-4716-8310-8a43635ca894" />
<option value="6971fc05-6110-4b33-ac58-69628d336a48" />
<option value="6dffb37e-ecb0-4c89-8cbe-e38ed347c397" />
<option value="7216273c-1271-4ea2-88d8-177e139316f1" />
<option value="7237b251-2008-431b-b6b3-005dea48c3bf" />
<option value="72c65d6c-d369-4e6e-ad89-52749cab36a6" />
<option value="76858669-1b07-4bac-bc00-8448c673d06d" />
<option value="80ef4e51-2808-48f4-9166-bf883578c6f0" />
<option value="8589889d-a532-45c6-9ffe-0671c2d32583" />
<option value="8861f660-2aea-480d-a773-4e1f58fe9ac0" />
<option value="8fab86d7-b6d2-4ce8-9174-172bd49c7906" />
<option value="94d90579-6a6d-44a9-8b93-de694c0c38ef" />
<option value="9523ff40-4e2b-4c96-b52e-69093f946698" />
<option value="9ebc3bf3-3f18-425d-a4fb-c87a6a121c9a" />
<option value="aaf8bf1e-ce4a-48ab-a65e-6da5790fc566" />
<option value="ac68227b-a347-492a-8990-f2313a4c4838" />
<option value="b32841ec-424b-46e4-af25-b31a5ca4ae51" />
<option value="ba4f0602-d62b-4a9c-b86f-ee0dce40374d" />
<option value="c759a8ff-cf83-4618-90d4-31101dea7c77" />
<option value="e2907e39-fda1-4832-99ff-fb7407d64db3" />
<option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" />
<option value="ec11cbcb-475d-4b28-8eb3-f71715723d6c" />
<option value="ee1de16e-76b5-4384-8ed5-b2e3f4e3c2cb" />
<option value="f607f271-4885-4c4e-91da-e2caf2abd67a" />
<option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" />
</set>
</value>
</entry>
</map>
</option>
<pendingWorkingSetItems>
<entry key="1aa6c162-aaff-428f-bfae-3b53f8ce3f76">
<set>
<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/EvacuationMaps.kt" />
</set>
</entry>
<entry key="63e1cd83-39a1-4de9-83e4-a7d978cb9048" />
<entry key="9e78818c-2d23-4dc2-8dd8-c82d3ade6f9d" />
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" />
</pendingWorkingSetItems>
</component>
</project>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

10
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

2
.idea/vcs.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -50,6 +50,8 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
// Foundation (for verticalScroll, rememberScrollState, etc.)
implementation("androidx.compose.foundation:foundation:1.5.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@ -59,6 +61,15 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest)
//praktikum 1
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.compose.material:material-icons-extended:<versi-compose>")
// Navigation for Compose
implementation("androidx.navigation:navigation-compose:2.6.0")
// AndroidX SplashScreen (required for installSplashScreen API)
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,26 +2,39 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:icon="@drawable/logo"
android:roundIcon="@drawable/logo"
android:supportsRtl="true"
android:theme="@style/Theme.PanicButton">
android:theme="@style/Theme.PanicButton"
android:label="@string/app_name">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PanicButton">
android:theme="@style/Theme.App.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Evacuation map activities -->
<activity
android:name=".EvacuationMapsActivity"
android:exported="false"
android:label="Peta Evakuasi" />
<activity
android:name=".EvacuationMapDetailActivity"
android:exported="false"
android:label="Detail Peta" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -0,0 +1,34 @@
package id.ac.ubharajaya.panicbutton
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
class EvacuationMapDetailActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val which = intent.getStringExtra("which") ?: "selatan"
val resId = if (which == "selatan") R.drawable.lantai_1_selatan else R.drawable.lantai_1_utara
setContent {
MaterialTheme {
EvacuationMapDetailContent(resId)
}
}
}
}
@Composable
fun EvacuationMapDetailContent(drawableResId: Int) {
Column(modifier = Modifier.fillMaxSize().padding(12.dp)) {
Text(text = "Detail Peta", modifier = Modifier.padding(bottom = 12.dp))
Image(painter = painterResource(id = drawableResId), contentDescription = "Map Detail", modifier = Modifier.fillMaxWidth().heightIn(max = 800.dp))
}
}

View File

@ -0,0 +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.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) {
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)
)
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 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
)
}
}
}

View File

@ -0,0 +1,68 @@
package id.ac.ubharajaya.panicbutton
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class EvacuationMapsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
EvacuationMapsContent(onSelect = { which ->
val intent = Intent(this, EvacuationMapDetailActivity::class.java)
intent.putExtra("which", which)
startActivity(intent)
})
}
}
}
}
@Composable
fun EvacuationMapsContent(onSelect: (String) -> Unit) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text(text = "Peta Jalur Evakuasi", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 12.dp))
// Selatan
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { onSelect("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.")
}
}
}
// Utara
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { onSelect("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.")
}
}
}
}
}

View File

@ -1,52 +1,69 @@
package id.ac.ubharajaya.panicbutton
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import id.ac.ubharajaya.panicbutton.ui.theme.PanicButtonTheme
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.compose.material3.MaterialTheme
import com.google.android.gms.location.LocationServices
class MainActivity : ComponentActivity() {
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?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PanicButtonTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val fusedClient = LocationServices.getFusedLocationProviderClient(this)
val openEvacMaps = {
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 {
MaterialTheme {
MainScreen(viewModel = viewModel, onOpenEvacMaps = openEvacMaps, onSendAlert = ::handleSendAlert)
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
PanicButtonTheme {
Greeting("Android")
}
}

View File

@ -0,0 +1,484 @@
package id.ac.ubharajaya.panicbutton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.style.TextAlign
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit, onSendAlert: () -> Unit) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
// Put the emergency box in the topBar so Scaffold will position it below the status bar automatically
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 8.dp),
contentAlignment = Alignment.TopCenter
) {
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 -> // 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 = contentPaddingModifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(12.dp))
// Main card and other content
EmergencyConditionCard(viewModel)
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = onOpenEvacMaps,
modifier = Modifier
.fillMaxWidth(0.6f)
.height(48.dp),
shape = RoundedCornerShape(12.dp)
) {
Text(text = "Lihat Peta Evakuasi")
}
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 notifikasi
viewModel.dialogMessage
?.takeIf { it.isNotBlank() }
?.let { msg ->
AlertDialog(
onDismissRequest = { viewModel.clearDialog() },
confirmButton = {
TextButton(onClick = { viewModel.clearDialog() }) { Text("OK") }
},
title = { Text("🚨 Notifikasi", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)) },
text = { Text(msg) }
)
}
}
}
// =======================================================
// EmergencyConditionCard
// =======================================================
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmergencyConditionCard(viewModel: MainViewModel) {
var expanded by remember { mutableStateOf(false) }
Card(
shape = RoundedCornerShape(24.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(24.dp),
clip = true
)
) {
Column(
modifier = Modifier
.padding(24.dp)
) {
// Header dengan gradient background
Row(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
MaterialTheme.colorScheme.secondary.copy(alpha = 0.05f)
)
),
shape = RoundedCornerShape(16.dp)
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Icon dengan background
Box(
modifier = Modifier
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = "🚨",
style = MaterialTheme.typography.titleLarge
)
}
Column {
Text(
text = "Jenis Kondisi Darurat",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.primary
)
Text(
text = "Pilih satu atau lebih kondisi",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp)
)
}
}
Spacer(Modifier.height(24.dp))
// Custom Styled Dropdown
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
shape = RoundedCornerShape(16.dp)
)
.border(
width = 1.dp,
color = if (expanded)
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
shape = RoundedCornerShape(16.dp)
)
.clickable { expanded = !expanded }
.padding(horizontal = 18.dp, vertical = 16.dp)
) {
Column {
// Selected items preview
val selectedItems = viewModel.options.filter { viewModel.isChecked(it.label) }
if (selectedItems.isEmpty()) {
Text(
"Pilih kondisi darurat...",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
"${selectedItems.size} kondisi terpilih",
style = MaterialTheme.typography.bodySmall.copy(
fontWeight = FontWeight.Medium
),
color = MaterialTheme.colorScheme.primary
)
// Show first 2 selected items as chips
selectedItems.take(2).forEach { opt ->
SelectedChip(option = opt, viewModel = viewModel)
}
if (selectedItems.size > 2) {
Text(
"+${selectedItems.size - 2} lainnya",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
// Dropdown Menu
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp)
)
) {
Column(
modifier = Modifier
.width(320.dp)
.padding(vertical = 8.dp)
.heightIn(max = 280.dp)
.verticalScroll(rememberScrollState())
) {
viewModel.options.forEach { opt ->
EmergencyOptionItem(
option = opt,
isChecked = viewModel.isChecked(opt.label),
onCheckedChange = { checked ->
viewModel.setChecked(opt.label, checked)
}
)
}
}
}
}
// Dropdown arrow
Icon(
imageVector = if (expanded)
Icons.Filled.KeyboardArrowUp
else
Icons.Filled.KeyboardArrowDown,
contentDescription = if (expanded) "Tutup" else "Buka",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.align(Alignment.CenterEnd)
)
}
Spacer(Modifier.height(20.dp))
// Extra note input dengan styling yang lebih baik
Column {
Text(
"Catatan Tambahan",
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium
),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 8.dp, start = 4.dp)
)
OutlinedTextField(
value = viewModel.otherNote,
onValueChange = { viewModel.otherNote = it },
placeholder = {
Text(
"Tambahkan catatan jika diperlukan...",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.dp, max = 120.dp),
shape = RoundedCornerShape(14.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f),
cursorColor = MaterialTheme.colorScheme.primary,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
unfocusedTextColor = MaterialTheme.colorScheme.onSurface
),
textStyle = MaterialTheme.typography.bodyMedium
)
// Validation jika pilih 'Lainnya' tapi kosong
if (viewModel.isChecked("Lainnya") && viewModel.otherNote.isBlank()) {
Row(
modifier = Modifier.padding(top = 6.dp, start = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Warning",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
Text(
"Catatan wajib diisi jika memilih 'Lainnya'",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}
// =======================================================
// EmergencyOptionItem
// =======================================================
@Composable
fun EmergencyOptionItem(
option: EmergencyOption,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onCheckedChange(!isChecked) }
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
// Icon dengan background
Box(
modifier = Modifier
.size(36.dp)
.background(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = option.icon,
style = MaterialTheme.typography.bodyLarge
)
}
Spacer(Modifier.width(12.dp))
// Label
Text(
text = option.label,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = if (isChecked) FontWeight.Medium else FontWeight.Normal
),
color = if (isChecked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(8.dp))
// Custom Checkbox
Box(
modifier = Modifier
.size(20.dp)
.background(
color = if (isChecked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(4.dp)
)
.border(
width = 1.dp,
color = if (isChecked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.outline.copy(alpha = 0.6f),
shape = RoundedCornerShape(4.dp)
),
contentAlignment = Alignment.Center
) {
if (isChecked) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(14.dp)
)
}
}
}
}
// =======================================================
// SelectedChip
// =======================================================
@Composable
fun SelectedChip(option: EmergencyOption, viewModel: MainViewModel) {
Box(
modifier = Modifier
.wrapContentWidth()
.background(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${option.icon} ${option.label}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
maxLines = 1
)
Spacer(Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Hapus",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(14.dp)
.clickable { viewModel.setChecked(option.label, false) }
)
}
}
}

View File

@ -0,0 +1,80 @@
package id.ac.ubharajaya.panicbutton
import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
data class EmergencyOption(
val label: String,
val icon: String
)
class MainViewModel : ViewModel() {
val options = listOf(
EmergencyOption("Kebakaran", "🔥"),
EmergencyOption("Banjir", "🌊"),
EmergencyOption("Gempa Bumi", "🌍"),
EmergencyOption("Huru Hara/Demonstrasi", "💥"),
EmergencyOption("Lainnya", "✏️")
)
// observable map so Compose recomposes on changes
private val _checkedState = mutableStateMapOf<String, Boolean>().apply {
options.forEach { put(it.label, false) }
}
var otherNote by mutableStateOf("")
var dialogMessage by mutableStateOf<String?>(null)
fun isChecked(label: String): Boolean = _checkedState[label] == true
fun setChecked(label: String, isChecked: Boolean) {
_checkedState[label] = isChecked
if (label == "Lainnya" && !isChecked) {
otherNote = ""
}
}
fun sendAlert(latitude: Double? = null, longitude: Double? = null) {
val selectedOptions = options.filter { isChecked(it.label) }.map { it.label }
if (selectedOptions.isEmpty()) {
dialogMessage = "Pilih setidaknya satu kondisi darurat."
return
}
if (isChecked("Lainnya") && otherNote.isBlank()) {
dialogMessage = "Catatan wajib diisi jika Anda memilih 'Lainnya'."
return
}
val bodyBuilder = StringBuilder()
bodyBuilder.append("Kondisi: ${selectedOptions.joinToString(", ")}")
if (otherNote.isNotBlank()) {
bodyBuilder.append("\nCatatan: ${otherNote.trim()}")
}
if (latitude != null && longitude != null) {
bodyBuilder.append("\nLokasi: https://maps.google.com/?q=$latitude,$longitude")
}
val payload = bodyBuilder.toString()
viewModelScope.launch {
try {
val resultMessage = NotificationSender.sendNotification(payload)
dialogMessage = resultMessage
} catch (e: Exception) {
e.printStackTrace()
dialogMessage = "Gagal mengirim laporan. Silakan periksa koneksi Anda dan coba lagi."
}
}
}
fun clearDialog() {
dialogMessage = null
}
}

View File

@ -0,0 +1,75 @@
package id.ac.ubharajaya.panicbutton
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
object NotificationSender {
private val client = OkHttpClient.Builder()
.build()
private const val server = "https://ntfy.ubharajaya.ac.id"
private const val topic = "panic-button"
private const val TAG = "NotificationSender"
private fun sanitizeHeaderValue(value: String): String {
val sb = StringBuilder()
for (ch in value) {
val code = ch.code
if (code in 32..126) sb.append(ch)
}
val out = sb.toString()
return if (out.isBlank()) "Alert" else out
}
suspend fun sendNotification(message: String): String = withContext(Dispatchers.IO) {
try {
val url = "$server/$topic"
Log.d(TAG, "Preparing notification to $url")
val titleSafe = sanitizeHeaderValue("🚨 Alert 🚨")
val cleanMessage = message.trim()
val body = cleanMessage.toRequestBody("text/plain".toMediaType())
val request = Request.Builder()
.url(url)
.addHeader("Title", titleSafe)
.addHeader("Priority", "urgent")
.addHeader("Tags", "warning")
.post(body)
.build()
// Debug logging
Log.d(TAG, "Body to send: $cleanMessage")
client.newCall(request).execute().use { resp ->
val respCode = resp.code
val respMessage = resp.message
val respBody = try {
resp.body?.string()
} catch (e: Exception) {
null
}
Log.d(TAG, "Response: code=$respCode message=$respMessage body=$respBody")
return@withContext if (resp.isSuccessful) {
"Notifikasi berhasil dikirim!"
} else {
"Gagal mengirim notifikasi: $respCode $respMessage ${respBody ?: ""}".trim()
}
}
} catch (e: IOException) {
Log.e(TAG, "IO error", e)
return@withContext "Gagal mengirim notifikasi: ${e.localizedMessage}"
} catch (e: Exception) {
Log.e(TAG, "Unexpected error", e)
return@withContext "Gagal mengirim notifikasi: ${e.localizedMessage}"
}
}
}

View File

@ -0,0 +1,58 @@
package id.ac.ubharajaya.panicbutton
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.Dp
@Composable
fun PanicButton(onClick: () -> Unit, buttonSize: Dp = 170.dp, shadowSize: Dp = 210.dp) {
val scaleFactor = buttonSize.value / 170f
val panicColor = Color(0xFFB71C1C)
val darkShade = Color(0xFF7F0F0F)
val lightAccent = Color(0xFFFF8A80)
val interactionSource = remember { MutableInteractionSource() }
val isPressedState = interactionSource.collectIsPressedAsState()
val isPressed = isPressedState.value
val scaleAnim = animateFloatAsState(targetValue = if (isPressed) 0.96f else 1f, animationSpec = tween(120)).value
val elevationAnim = animateDpAsState(targetValue = if (isPressed) 6.dp else 18.dp, animationSpec = tween(120)).value
val gradient = Brush.verticalGradient(listOf(lightAccent, panicColor, darkShade))
Box(contentAlignment = Alignment.Center) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(buttonSize)
.scale(scaleAnim)
.shadow(elevation = elevationAnim * scaleFactor, shape = CircleShape)
.background(brush = gradient, shape = CircleShape)
.clickable(indication = null, interactionSource = interactionSource) { onClick() }
) {
// highlight bubble (scaled)
val highlightSize = 70.dp * scaleFactor
val highlightOffsetX = (-24).dp * scaleFactor
val highlightOffsetY = (-28).dp * scaleFactor
Box(modifier = Modifier.size(highlightSize).offset(x = highlightOffsetX, y = highlightOffsetY).background(color = Color.White.copy(alpha = 0.16f), shape = CircleShape))
val fontSize = (72f * scaleFactor).sp
Text("!", color = Color.White, fontSize = fontSize)
}
}
}

View File

@ -0,0 +1,3 @@
package id.ac.ubharajaya.panicbutton
data class ReportOption(val label: String, val icon: String)

View File

@ -2,10 +2,11 @@ package id.ac.ubharajaya.panicbutton.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Blue_primary = Color(0xFF007BFF)
val Blue_secondary = Color(0xFF00A2E8)
val Dark_blue = Color(0xFF0056b3)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Grey_light = Color(0xFFF5F5F5)
val Grey_dark = Color(0xFF333333)
val Red_cancel = Color(0xFFDC3545)

View File

@ -1,53 +1,53 @@
package id.ac.ubharajaya.panicbutton.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
primary = Blue_secondary,
secondary = Blue_primary,
background = Grey_dark,
surface = Color(0xFF1E1E1E),
onPrimary = Grey_dark,
onSecondary = Grey_dark,
onBackground = Grey_light,
onSurface = Grey_light,
error = Red_cancel
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
primary = Blue_primary,
secondary = Blue_secondary,
tertiary = Dark_blue,
background = Grey_light,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
onBackground = Grey_dark,
onSurface = Grey_dark,
error = Red_cancel
)
@Composable
fun PanicButtonTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
dynamicColor: Boolean = false, // Disable dynamic color
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
val view = LocalView.current
if (!view.isInEditMode) {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
MaterialTheme(

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M507.49,426.07L282.86,53.54c-5.68,-9.41 -15.87,-15.17 -26.86,-15.17c-10.99,0 -21.19,5.76 -26.86,15.17L4.51,426.07c-5.84,9.69 -6.01,21.77 -0.45,31.63c5.56,9.85 16,15.94 27.32,15.94h449.26c11.31,0 21.75,-6.09 27.32,-15.94C513.51,447.84 513.34,435.76 507.49,426.07z"
android:fillColor="#FF9900"/>
<path
android:pathData="M256,38.37c-10.99,0 -21.19,5.76 -26.86,15.17L4.51,426.07c-5.84,9.69 -6.01,21.77 -0.45,31.63c5.56,9.85 16,15.94 27.32,15.94h224.63L256,38.37L256,38.37z"
android:fillColor="#FFDC35"/>
<path
android:pathData="M445.33,432.79H67.11c-3.59,0 -6.91,-1.91 -8.72,-5.01c-1.81,-3.1 -1.83,-6.93 -0.05,-10.06L247.23,85.03c1.79,-3.15 5.14,-5.11 8.77,-5.11c0,0 0,0 0,0c3.63,0 6.97,1.95 8.77,5.1l189.32,332.69c1.78,3.12 1.76,6.95 -0.05,10.06S448.92,432.79 445.33,432.79zM84.44,412.62h343.54L256.01,110.42L84.44,412.62z"
android:fillColor="#F20013"/>
<path
android:pathData="M256.33,412.62H84.44l171.58,-302.19l-0.01,-30.5h-0c-3.63,0 -6.98,1.95 -8.77,5.11L58.34,417.72c-1.77,3.12 -1.75,6.95 0.05,10.06c1.81,3.1 5.13,5.01 8.72,5.01h189.22v-20.17H256.33z"
android:fillColor="#FF4B00"/>
<path
android:pathData="M279.36,376.88c0,12.34 -10.54,23.18 -22.88,23.18c-13.25,0 -23.18,-10.84 -23.18,-23.18c0,-12.64 9.94,-23.18 23.18,-23.18C268.83,353.7 279.36,364.24 279.36,376.88zM273.64,319.68c0,9.33 -10.24,13.25 -17.46,13.25c-9.63,0 -17.76,-3.91 -17.76,-13.25c0,-35.83 -4.21,-87.31 -4.21,-123.13c0,-11.74 9.63,-18.36 21.98,-18.36c11.74,0 21.68,6.62 21.68,18.36C277.86,232.37 273.64,283.86 273.64,319.68z"
android:fillColor="#533F29"/>
</vector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ubhara"
android:top="50dp"
android:bottom="50dp"
android:left="50dp"
android:right="50dp"/>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@android:color/transparent" />
<foreground android:drawable="@drawable/logo" />
<monochrome android:drawable="@drawable/logo" />
</adaptive-icon>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@android:color/transparent" />
<foreground android:drawable="@drawable/logo" />
<monochrome android:drawable="@drawable/logo" />
</adaptive-icon>

View File

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