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