diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c53de8a..f685a40 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -63,5 +63,8 @@ dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("androidx.compose.material3:material3:1.1.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a12cf55..ca2c22b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
+
(ScreenState.Main) }
val context = LocalContext.current
- val sharedPreferences = context.getSharedPreferences("PanicButtonPrefs", Context.MODE_PRIVATE)
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Panic Button") },
+ // Logika TopBar hanya muncul jika di MainScreen,
+ // SettingsScreen memiliki TopBar sendiri
+ actions = {
+ if (currentScreen is ScreenState.Main) {
+ IconButton(onClick = {
+ currentScreen = ScreenState.Settings
+ }) {
+ Icon(Icons.Filled.Settings, contentDescription = "Settings")
+ }
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ when (currentScreen) {
+ is ScreenState.Main -> MainScreen(
+ paddingValues = paddingValues,
+ onSendNotification = { conditions, report, onResult ->
+ sendNotification(context, conditions, report, onResult)
+ },
+ onNavigateEvakuasi = {
+ val intent = Intent(context, JalurEvakuasiActivity::class.java)
+ context.startActivity(intent)
+ }
+ )
+ is ScreenState.Settings -> SettingsScreen(
+ // Melewatkan fungsi untuk kembali
+ onBack = { currentScreen = ScreenState.Main }
+ )
+ }
+ }
+}
+
+
+// ----------------------------------------------------------------------
+// 4. MAIN SCREEN
+// ----------------------------------------------------------------------
+
+@Composable
+fun MainScreen(
+ paddingValues: PaddingValues,
+ onSendNotification: (String, String, (String) -> Unit) -> Unit,
+ onNavigateEvakuasi: () -> Unit
+) {
+ val focusManager = LocalFocusManager.current
var message by remember { mutableStateOf("Klik tombol untuk mengirim notifikasi") }
var selectedConditions by remember { mutableStateOf(mutableSetOf()) }
var additionalNotes by remember { mutableStateOf(TextFieldValue("")) }
- var showDialog by remember { mutableStateOf(false) }
- var dialogMessage by remember { mutableStateOf("") }
- var showSettingsDialog by remember { mutableStateOf(false) }
-
- // Load saved configuration
- var ntfyServer by remember {
- mutableStateOf(sharedPreferences.getString("ntfyServer", "https://ntfy.sh") ?: "https://ntfy.sh")
- }
- var ntfyTopic by remember {
- mutableStateOf(sharedPreferences.getString("ntfyTopic", "ubahara-panic-button") ?: "ubahara-panic-button")
- }
val scrollState = rememberScrollState()
- // Dialog konfirmasi
- if (showDialog) {
- AlertDialog(
- onDismissRequest = { showDialog = false },
- title = { Text("Konfirmasi") },
- text = { Text(dialogMessage) },
- confirmButton = {
- Button(onClick = { showDialog = false }) {
- Text("OK")
- }
- }
- )
- }
-
- // Dialog Settings
- if (showSettingsDialog) {
- var tempServer by remember { mutableStateOf(ntfyServer) }
- var tempTopic by remember { mutableStateOf(ntfyTopic) }
-
- AlertDialog(
- onDismissRequest = { showSettingsDialog = false },
- title = { Text("Pengaturan NTFY") },
- text = {
- Column {
- Text("Server NTFY:")
- OutlinedTextField(
- value = tempServer,
- onValueChange = { tempServer = it },
- modifier = Modifier.fillMaxWidth()
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text("Topic:")
- OutlinedTextField(
- value = tempTopic,
- onValueChange = { tempTopic = it },
- modifier = Modifier.fillMaxWidth()
- )
- }
- },
- confirmButton = {
- Button(onClick = {
- ntfyServer = tempServer
- ntfyTopic = tempTopic
- // Save to SharedPreferences
- with(sharedPreferences.edit()) {
- putString("ntfyServer", tempServer)
- putString("ntfyTopic", tempTopic)
- // Gabungkan untuk kompatibilitas dengan AlertActivity
- putString("urlKey", "$tempServer/$tempTopic")
- apply()
- }
- showSettingsDialog = false
- message = "Pengaturan berhasil disimpan"
- }) {
- Text("Simpan")
- }
- },
- dismissButton = {
- TextButton(onClick = { showSettingsDialog = false }) {
- Text("Batal")
- }
- }
- )
- }
-
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
+ .padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
- // Header dengan tombol settings
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "Terjadi Kondisi Darurat",
- fontSize = 20.sp,
- color = Color.Red
- )
- Button(
- onClick = { showSettingsDialog = true },
- colors = ButtonDefaults.buttonColors(containerColor = Color.Gray)
- ) {
- Text("βοΈ", fontSize = 18.sp)
- }
- }
-
- Spacer(modifier = Modifier.height(8.dp))
Text(
- text = "Server: $ntfyServer\nTopic: $ntfyTopic",
- fontSize = 12.sp,
- color = Color.Gray,
+ text = "Terjadi Kondisi Darurat",
+ fontSize = 20.sp,
+ color = Color.Red,
modifier = Modifier.padding(bottom = 16.dp)
)
listOf(
- "π₯ Kebakaran", "βοΈ Banjir", "π Tsunami", "π Gunung Meletus",
- "π Gempa Bumi", "πΏ Huru hara", "π Binatang Buas",
- "β’οΈ Radiasi Nuklir", "β£οΈ Biohazard"
+ "\uD83D\uDD25 Kebakaran", "β\uFE0F Banjir", "\uD83C\uDF0A Tsunami", "\uD83C\uDF0B Gunung Meletus",
+ "\uD83C\uDF0F Gempa Bumi", "\uD83D\uDC7F Huru hara", "\uD83D\uDC0D Binatang Buas",
+ "β’\uFE0F Radiasi Nuklir", "β£\uFE0F Biohazard"
).forEach { condition ->
Row(
modifier = Modifier
@@ -212,23 +217,18 @@ fun MyApp() {
val notes = additionalNotes.text
val conditions = selectedConditions.joinToString(", ")
val report = "Kondisi: $conditions\nCatatan: $notes"
- val fullUrl = "$ntfyServer/$ntfyTopic"
-
- sendNotification(fullUrl, conditions, report) { response ->
+ onSendNotification(conditions, report) { response ->
message = response
- dialogMessage = response
- showDialog = true
}
},
- colors = ButtonDefaults.buttonColors(containerColor = Color.Red),
- enabled = selectedConditions.isNotEmpty()
+ colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) {
Text(text = "Kirim Laporan", color = Color.White)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "JANGAN PANIK! SEGERA EVAKUASI\nDIRI ANDA KE TITIK KUMPUL",
+ text = "βJANGAN PANIK! SEGERA EVAKUASI\nDIRI ANDA KE TITIK KUMPULβ",
color = Color.Red,
fontSize = 15.sp
)
@@ -236,10 +236,7 @@ fun MyApp() {
Spacer(modifier = Modifier.height(16.dp))
Text(text = message, Modifier.padding(top = 16.dp))
Button(
- onClick = {
- val intent = Intent(context, JalurEvakuasiActivity::class.java)
- context.startActivity(intent)
- },
+ onClick = onNavigateEvakuasi,
colors = ButtonDefaults.buttonColors(containerColor = Color.Green)
) {
Text(text = "Lihat Jalur Evakuasi", color = Color.White)
@@ -247,19 +244,122 @@ fun MyApp() {
}
}
-fun sendNotification(url: String, condition: String, report: String, onResult: (String) -> Unit) {
+
+// ----------------------------------------------------------------------
+// 5. SETTINGS SCREEN (DIPERBAIKI)
+// ----------------------------------------------------------------------
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsScreen(onBack: () -> Unit) {
+ val context = LocalContext.current
+ val prefs = remember { getPrefs(context) }
+
+ // Inisialisasi state dari SharedPreferences
+ var serverText by remember {
+ mutableStateOf(TextFieldValue(prefs.getString(KEY_NTFY_SERVER, DEFAULT_NTFY_SERVER) ?: DEFAULT_NTFY_SERVER))
+ }
+ var topicText by remember {
+ mutableStateOf(TextFieldValue(prefs.getString(KEY_NTFY_TOPIC, DEFAULT_NTFY_TOPIC) ?: DEFAULT_NTFY_TOPIC))
+ }
+
+ var saveMessage by remember { mutableStateOf("") }
+
+ // β¬
οΈ TAMBAHAN: Menangani tombol kembali pada perangkat
+ BackHandler(onBack = onBack)
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Pengaturan NTFY Endpoint") },
+ navigationIcon = {
+ IconButton(onClick = onBack) { // β¬
οΈ onBack menangani navigasi kembali ke MainScreen
+ Icon(Icons.Filled.ArrowBack, contentDescription = "Kembali")
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(
+ text = "Konfigurasi Server Notifikasi",
+ fontSize = 18.sp,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+
+ OutlinedTextField(
+ value = serverText,
+ onValueChange = { serverText = it },
+ label = { Text("Server NTFY (contoh: https://ntfy.sh)") },
+ modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+ singleLine = true
+ )
+
+ OutlinedTextField(
+ value = topicText,
+ onValueChange = { topicText = it },
+ label = { Text("Topic (contoh: panic-button)") },
+ modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ singleLine = true
+ )
+
+ Button(
+ onClick = {
+ saveNtfyConfig(
+ context,
+ serverText.text.trim(),
+ topicText.text.trim()
+ )
+ saveMessage = "Konfigurasi berhasil disimpan! URL: ${getNtfyUrl(context)}"
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Simpan Konfigurasi")
+ }
+
+ Text(
+ text = saveMessage,
+ color = if (saveMessage.contains("berhasil")) Color.Green else Color.Black,
+ modifier = Modifier.padding(top = 16.dp)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = "URL saat ini: ${getNtfyUrl(context)}",
+ style = TextStyle(color = Color.Gray, fontSize = 12.sp)
+ )
+ }
+ }
+}
+
+
+// ----------------------------------------------------------------------
+// 6. SEND NOTIFICATION FUNCTION
+// ----------------------------------------------------------------------
+
+fun sendNotification(context: Context, condition: String, report: String, onResult: (String) -> Unit) {
val client = OkHttpClient()
+ val url = getNtfyUrl(context)
val tagMapping = mapOf(
- "π₯ Kebakaran" to "fire",
- "βοΈ Banjir" to "cloud_with_lightning_and_rain",
- "π Tsunami" to "ocean",
- "π Gunung Meletus" to "volcano",
- "π Gempa Bumi" to "earth_asia",
- "πΏ Huru hara" to "imp",
- "π Binatang Buas" to "snake",
- "β’οΈ Radiasi Nuklir" to "radioactive",
- "β£οΈ Biohazard" to "biohazard"
+ "\uD83D\uDD25 Kebakaran" to "fire",
+ "β\uFE0F Banjir" to "cloud_with_lightning_and_rain",
+ "\uD83C\uDF0A Tsunami" to "ocean",
+ "\uD83C\uDF0B Gunung Meletus" to "volcano",
+ "\uD83C\uDF0F Gempa Bumi" to "earth_asia",
+ "\uD83D\uDC7F Huru hara" to "imp",
+ "\uD83D\uDC0D Binatang Buas" to "snake",
+ "β’\uFE0F Radiasi Nuklir" to "radioactive",
+ "β£\uFE0F Biohazard" to "biohazard"
)
val selectedList = condition
@@ -269,7 +369,8 @@ fun sendNotification(url: String, condition: String, report: String, onResult: (
val cleanConditionText = selectedList.joinToString(", ")
val emojiTags = selectedList.mapNotNull { tagMapping[it] }
- val finalTags = listOf("alert", "warning") + emojiTags
+
+ val finalTags = listOf("Alert") + emojiTags
val finalReport = "Kondisi: $cleanConditionText\nCatatan: ${report.substringAfter("Catatan:")}"
val requestBody = RequestBody.create(
@@ -278,7 +379,7 @@ fun sendNotification(url: String, condition: String, report: String, onResult: (
)
val request = Request.Builder()
.url(url)
- .addHeader("Title", "β οΈ ALERT - Kondisi Darurat")
+ .addHeader("Title", "Alert")
.addHeader("Priority", "urgent")
.addHeader("Tags", finalTags.joinToString(","))
.post(requestBody)
@@ -288,12 +389,12 @@ fun sendNotification(url: String, condition: String, report: String, onResult: (
try {
val response = client.newCall(request).execute()
if (response.isSuccessful) {
- onResult("β
Notifikasi berhasil dikirim!\nLaporan telah terkirim ke sistem.")
+ onResult("Notifikasi berhasil dikirim ke $url!")
} else {
- onResult("β Gagal mengirim notifikasi\nKode error: ${response.code}")
+ onResult("Gagal mengirim notifikasi ke $url: ${response.code}")
}
} catch (e: Exception) {
- onResult("β Error: ${e.message}")
+ onResult("Error: ${e.message}")
}
}.start()
}
\ No newline at end of file