diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml
index dbdf9f5..6aa0d3a 100644
--- a/.idea/copilot.data.migration.agent.xml
+++ b/.idea/copilot.data.migration.agent.xml
@@ -16,6 +16,12 @@
+
+
+
+
+
+
@@ -25,6 +31,11 @@
+
+
+
+
+
diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml
deleted file mode 100644
index a2b9b33..0000000
--- a/.idea/copilotDiffState.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt
index 9ff81ac..be4d47b 100644
--- a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt
@@ -3,370 +3,18 @@ package id.ac.ubharajaya.panicbutton
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-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.selection.toggleable
-import androidx.compose.foundation.shape.CircleShape
-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.draw.scale
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody.Companion.toRequestBody
+import androidx.activity.viewModels
+import androidx.compose.material3.MaterialTheme
class MainActivity : ComponentActivity() {
+ private val viewModel: MainViewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
- MyApp()
- }
- }
-}
-
-
-@Composable
-fun MyApp() {
- // dialogMessage akan menampung hasil callback dari sendNotification; null = tidak tampil
- var dialogMessage by remember { mutableStateOf(null) }
-
- // theme color for panic
- val panicColor = Color(0xFFB71C1C)
-
- // Report options (checkbox-style) with icons (emoji from ntfy emoji list)
- val reportOptions = listOf(
- "Kebakaran" to "🔥",
- "Banjir" to "🌊",
- "Gempa Bumi" to "🌍",
- "Huru Hara/Demostrasi" to "📣",
- "Lainnya" to "📝"
- )
- val checkedMap = remember { mutableStateMapOf().apply { reportOptions.forEach { put(it.first, false) } } }
- var otherNote by remember { mutableStateOf("") }
-
- // UI
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(18.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- // Checklist area styled as card
- Surface(
- color = Color(0xFFFFEBEE), // soft red/pink background to match panic theme
- tonalElevation = 4.dp,
- shape = RoundedCornerShape(10.dp),
- modifier = Modifier
- .fillMaxWidth(0.92f)
- .padding(horizontal = 12.dp)
- ) {
- Column(modifier = Modifier.padding(14.dp)) {
- Text(
- text = "Terjadi Kondisi Darurat",
- fontWeight = FontWeight.SemiBold,
- fontSize = 18.sp,
- color = panicColor
- )
-
- Spacer(modifier = Modifier.height(10.dp))
-
- // Each option as a row with icon, label and checkbox aligned nicely
- reportOptions.forEach { (label, icon) ->
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 6.dp)
- .toggleable(
- value = checkedMap[label] ?: false,
- onValueChange = { checked -> checkedMap[label] = checked }
- )
- ) {
- // icon circle
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier
- .size(34.dp)
- .background(color = Color.White.copy(alpha = 0.9f), shape = CircleShape)
- .shadow(1.dp, shape = CircleShape)
- ) {
- Text(text = icon, fontSize = 18.sp)
- }
-
- Spacer(modifier = Modifier.width(12.dp))
-
- Column(modifier = Modifier.weight(1f)) {
- Text(text = label, fontSize = 16.sp)
- }
-
- Checkbox(
- checked = checkedMap[label] ?: false,
- onCheckedChange = { checked -> checkedMap[label] = checked },
- colors = CheckboxDefaults.colors(
- checkedColor = panicColor,
- uncheckedColor = Color.DarkGray
- )
- )
- }
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- OutlinedTextField(
- value = otherNote,
- onValueChange = { otherNote = it },
- label = { Text("Catatan tambahan (opsional)") },
- placeholder = { Text("Catatan tambahan ...") },
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(min = 56.dp, max = 140.dp),
- maxLines = 4
- )
-
- Spacer(modifier = Modifier.height(8.dp))
-
- // small hint when Lainnya is checked but note empty
- if ((checkedMap["Lainnya"] == true) && otherNote.isBlank()) {
- Text(
- text = "Catatan wajib jika Anda memilih 'Lainnya'",
- color = panicColor,
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(top = 6.dp)
- )
- }
- }
- }
-
- Spacer(modifier = Modifier.height(20.dp))
-
- // Keep the existing nice PanicButton unchanged (slightly smaller spacing)
- PanicButton(onClick = {
- // Validation: at least one option selected
- val selected = reportOptions.map { it.first }.filter { checkedMap[it] == true }
- if (selected.isEmpty()) {
- dialogMessage = "Pilih minimal satu jenis laporan sebelum mengirim."
- return@PanicButton
- }
- if (selected.contains("Lainnya") && otherNote.isBlank()) {
- dialogMessage = "Untuk opsi 'Lainnya', harap tambahkan catatan yang menjelaskan keadaan."
- return@PanicButton
- }
-
- // build message: list selected options and the note
- val message = buildString {
- append("Jenis Laporan: ")
- append(selected.joinToString(", "))
- append("\n")
- append("Keterangan: ")
- if (otherNote.isNotBlank()) append(otherNote.trim()) else append("-")
- append("\n")
- append("Pengirim: Rakha adi saputro 202310715083")
- }
-
- sendNotification(message) { response -> dialogMessage = response }
- })
-
- Spacer(modifier = Modifier.height(14.dp))
-
- // dialog hasil
- if (dialogMessage != null) {
- Dialog(onDismissRequest = { dialogMessage = null }) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(24.dp),
- contentAlignment = Alignment.Center
- ) {
- Surface(
- shape = RoundedCornerShape(12.dp),
- color = Color.White,
- tonalElevation = 8.dp,
- modifier = Modifier.wrapContentWidth()
- ) {
- Column(
- modifier = Modifier
- .padding(0.dp)
- .widthIn(min = 280.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- // header
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .background(color = panicColor)
- .padding(vertical = 12.dp),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = "Notifikasi",
- color = Color.White,
- fontWeight = FontWeight.Bold,
- fontSize = 18.sp
- )
- }
-
- Spacer(modifier = Modifier.height(12.dp))
-
- // message
- Text(
- text = dialogMessage ?: "",
- color = Color.Black,
- modifier = Modifier.padding(horizontal = 16.dp)
- )
-
- Spacer(modifier = Modifier.height(20.dp))
-
- // action
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- contentAlignment = Alignment.Center
- ) {
- Button(
- onClick = { dialogMessage = null },
- colors = ButtonDefaults.buttonColors(containerColor = panicColor)
- ) {
- Text(text = "OK", color = Color.White)
- }
- }
- }
- }
- }
+ MaterialTheme {
+ MainScreen(viewModel)
}
}
}
}
-
-
-// New: 3D-styled PanicButton with press animation and glossy highlight
-@Composable
-fun PanicButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier
-) {
- // theme colors
- val panicColor = Color(0xFFB71C1C)
- val darkShade = Color(0xFF7F0F0F)
- val lightAccent = Color(0xFFFF8A80)
-
- // interaction for press-state
- val interactionSource = remember { MutableInteractionSource() }
- val isPressed by interactionSource.collectIsPressedAsState()
-
- // animations: scale down and lower elevation when pressed
- val scaleAnim by animateFloatAsState(targetValue = if (isPressed) 0.96f else 1f, animationSpec = tween(120))
- val elevationAnim by animateDpAsState(targetValue = if (isPressed) 6.dp else 18.dp, animationSpec = tween(120))
-
- // vertical gradient to simulate 3D lighting
- val gradient = Brush.verticalGradient(listOf(lightAccent, panicColor, darkShade))
-
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = modifier
- ) {
- Box(contentAlignment = Alignment.Center) {
- // soft outer shadow (ground shadow)
- Box(
- modifier = Modifier
- .size(210.dp)
- .shadow(elevation = elevationAnim, shape = CircleShape)
- .background(color = Color.Black.copy(alpha = 0.12f), shape = CircleShape)
- )
-
- // main 3D button
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier
- .size(170.dp)
- .scale(scaleAnim)
- .shadow(elevation = elevationAnim, shape = CircleShape)
- .background(brush = gradient, shape = CircleShape)
- .clickable(indication = null, interactionSource = interactionSource) {
- onClick()
- }
- ) {
- // glossy highlight (small white circle offset to top-left)
- Box(
- modifier = Modifier
- .size(70.dp)
- .offset(x = (-24).dp, y = (-28).dp)
- .background(color = Color.White.copy(alpha = 0.16f), shape = CircleShape)
- )
-
- // central exclamation icon
- Text(
- text = "!",
- color = Color.White,
- fontSize = 72.sp,
- fontWeight = FontWeight.ExtraBold
- )
-
- // subtle inner rim to enhance 3D edge
- Box(
- modifier = Modifier
- .matchParentSize()
- .padding(6.dp)
- .background(color = Color.Transparent, shape = CircleShape)
- )
- }
- }
- }
-}
-
-
-// Fungsi untuk mengirimkan HTTP request (mengikuti format pesan yang lebih rapi)
-fun sendNotification(message: String, onResult: (String) -> Unit) {
- val client = OkHttpClient()
- val url = "https://ntfy.ubharajaya.ac.id/panic-button" // Ganti dengan topik Anda
-
-
-
- val requestBody = message.toRequestBody(
- "text/plain".toMediaType()
- )
-
-
- val request = Request.Builder()
- .url(url)
- .addHeader("Title", "Alert")
- .addHeader("Priority", "urgent")
- .addHeader("Tags", "alert warning,rotating_light")
- .post(requestBody)
- .build()
-
-
-
-
- // Eksekusi request di thread terpisah
- Thread {
- try {
- val response = client.newCall(request).execute()
- if (response.isSuccessful) {
- onResult("Notifikasi berhasil dikirim!")
- } else {
- onResult("Gagal mengirim notifikasi: ${response.code}")
- }
- } catch (e: Exception) {
- onResult("Error: ${e.message}")
- }
- }.start()
-}
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt
new file mode 100644
index 0000000..d86479c
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt
@@ -0,0 +1,100 @@
+package id.ac.ubharajaya.panicbutton
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun MainScreen(viewModel: MainViewModel) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(18.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ tonalElevation = 4.dp,
+ shape = RoundedCornerShape(10.dp),
+ modifier = Modifier
+ .fillMaxWidth(0.92f)
+ .padding(horizontal = 12.dp)
+ ) {
+ Column(modifier = Modifier.padding(14.dp)) {
+ Text(text = "Jenis Kondisi Darurat", fontSize = 18.sp, color = MaterialTheme.colorScheme.error)
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ // list options directly from viewModel
+ for (opt in viewModel.options) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(34.dp)
+ .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f), shape = CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(text = opt.icon)
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Text(text = opt.label, modifier = Modifier.weight(1f))
+
+ val checked = viewModel.isChecked(opt.label)
+ Checkbox(
+ checked = checked,
+ onCheckedChange = { checkedValue -> viewModel.setChecked(opt.label, checkedValue) }
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ OutlinedTextField(
+ value = viewModel.otherNote,
+ onValueChange = { newValue -> viewModel.otherNote = newValue },
+ label = { Text("Catatan tambahan (opsional)") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 56.dp, max = 140.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ if (viewModel.isChecked("Lainnya") && viewModel.otherNote.isBlank()) {
+ Text("Catatan wajib jika Anda memilih 'Lainnya'", color = MaterialTheme.colorScheme.error)
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ PanicButton(onClick = { viewModel.sendAlert() })
+
+ Spacer(modifier = Modifier.height(14.dp))
+
+ val dialog = viewModel.dialogMessage
+ if (!dialog.isNullOrBlank()) {
+ AlertDialog(
+ onDismissRequest = { viewModel.clearDialog() },
+ confirmButton = { TextButton(onClick = { viewModel.clearDialog() }) { Text("OK") } },
+ title = { Text("Notifikasi") },
+ text = { Text(dialog) }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt
new file mode 100644
index 0000000..66a32dc
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt
@@ -0,0 +1,69 @@
+package id.ac.ubharajaya.panicbutton
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+
+class MainViewModel : ViewModel() {
+ val options = listOf(
+ ReportOption("Kebakaran", "🔥"),
+ ReportOption("Banjir", "🌊"),
+ ReportOption("Gempa Bumi", "🌍"),
+ ReportOption("Huru Hara/Demonstrasi", "📣"),
+ ReportOption("Lainnya", "📝")
+ )
+
+ // simple state holders
+ var checkedMap by mutableStateOf(options.associate { it.label to false }.toMutableMap())
+ private set
+
+ var otherNote by mutableStateOf("")
+
+ var dialogMessage by mutableStateOf(null)
+
+ fun isChecked(label: String): Boolean = checkedMap[label] ?: false
+ fun setChecked(label: String, checked: Boolean) { toggleOption(label, checked) }
+
+ fun toggleOption(label: String, checked: Boolean) {
+ val copy = checkedMap.toMutableMap()
+ copy[label] = checked
+ checkedMap = copy
+ }
+
+ fun sendAlert(senderName: String = "Rakha adi saputro 202310715083") {
+ val selected = checkedMap.filterValues { it }.keys.toList()
+ if (selected.isEmpty()) {
+ dialogMessage = "Pilih minimal satu jenis laporan sebelum mengirim."
+ return
+ }
+ if (selected.contains("Lainnya") && otherNote.isBlank()) {
+ dialogMessage = "Untuk opsi 'Lainnya', harap tambahkan catatan yang menjelaskan keadaan."
+ return
+ }
+
+ val message = buildString {
+ append("Jenis Laporan: ")
+ append(selected.joinToString(", "))
+ append("\nKeterangan: ")
+ append(if (otherNote.isBlank()) "-" else otherNote.trim())
+ append("\nPengirim: ")
+ append(senderName)
+ }
+
+ viewModelScope.launch {
+ try {
+ val result = NotificationSender.sendNotification(message)
+ dialogMessage = result
+ } catch (e: Exception) {
+ dialogMessage = "Error: ${e.message}"
+ }
+ }
+ }
+
+ fun clearDialog() {
+ dialogMessage = null
+ }
+}
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt
new file mode 100644
index 0000000..2c9ddee
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt
@@ -0,0 +1,29 @@
+package id.ac.ubharajaya.panicbutton
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+
+object NotificationSender {
+ private val client = OkHttpClient()
+ private const val url = "https://ntfy.ubharajaya.ac.id/panic-button"
+
+ suspend fun sendNotification(message: String): String = withContext(Dispatchers.IO) {
+ val body = message.toRequestBody("text/plain".toMediaType())
+ val request = Request.Builder()
+ .url(url)
+ .addHeader("Title", "Alert")
+ .addHeader("Priority", "urgent")
+ .addHeader("Tags", "alert warning,rotating_light")
+ .post(body)
+ .build()
+
+ client.newCall(request).execute().use { resp ->
+ if (resp.isSuccessful) "Notifikasi berhasil dikirim!" else "Gagal mengirim notifikasi: ${resp.code}"
+ }
+ }
+}
+
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/PanicButton.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/PanicButton.kt
new file mode 100644
index 0000000..4cc8846
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/PanicButton.kt
@@ -0,0 +1,56 @@
+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
+
+@Composable
+fun PanicButton(onClick: () -> Unit) {
+ 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(
+ modifier = Modifier
+ .size(210.dp)
+ .shadow(elevation = elevationAnim, shape = CircleShape)
+ .background(color = Color.Black.copy(alpha = 0.12f), shape = CircleShape)
+ )
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(170.dp)
+ .scale(scaleAnim)
+ .shadow(elevation = elevationAnim, shape = CircleShape)
+ .background(brush = gradient, shape = CircleShape)
+ .clickable(indication = null, interactionSource = interactionSource) { onClick() }
+ ) {
+ Box(modifier = Modifier.size(70.dp).offset(x = (-24).dp, y = (-28).dp).background(color = Color.White.copy(alpha = 0.16f), shape = CircleShape))
+ Text("!", color = Color.White, fontSize = 72.sp)
+ }
+ }
+}
diff --git a/app/src/main/java/id/ac/ubharajaya/panicbutton/ReportOption.kt b/app/src/main/java/id/ac/ubharajaya/panicbutton/ReportOption.kt
new file mode 100644
index 0000000..8271a45
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/panicbutton/ReportOption.kt
@@ -0,0 +1,3 @@
+package id.ac.ubharajaya.panicbutton
+
+data class ReportOption(val label: String, val icon: String)