Add NTFY configuration settings and persistence using SharedPreferences

Komit ini mengimplementasikan fitur konfigurasi endpoint NTFY yang dinamis dan menyimpannya menggunakan SharedPreferences. Ini mencakup penambahan layar pengaturan (SettingsScreen), perbaikan navigasi kembali menggunakan BackHandler, dan pembaruan fungsi sendNotification agar membaca URL dari konfigurasi yang disimpan.
This commit is contained in:
dend 2025-11-28 00:26:58 +07:00
parent 9807c900dd
commit e2bd34be8a
3 changed files with 236 additions and 131 deletions

View File

@ -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")
}

View File

@ -4,6 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET" />
<application

View File

@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.compose.BackHandler // ⬅️ Tambahkan impor ini
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -27,6 +28,47 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.ui.text.TextStyle
// ----------------------------------------------------------------------
// 1. CONSTANTS & HELPER FUNCTIONS FOR SHAREDPREFERENCES
// ----------------------------------------------------------------------
const val PREFS_NAME = "AppPrefs"
const val KEY_NTFY_SERVER = "ntfyServer"
const val KEY_NTFY_TOPIC = "ntfyTopic"
const val DEFAULT_NTFY_SERVER = "https://ntfy.ubharajaya.ac.id"
const val DEFAULT_NTFY_TOPIC = "panic-button"
fun getPrefs(context: Context) = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
fun saveNtfyConfig(context: Context, server: String, topic: String) {
getPrefs(context).edit().apply {
putString(KEY_NTFY_SERVER, server)
putString(KEY_NTFY_TOPIC, topic)
apply()
}
}
fun getNtfyUrl(context: Context): String {
val prefs = getPrefs(context)
val server = prefs.getString(KEY_NTFY_SERVER, DEFAULT_NTFY_SERVER)
val topic = prefs.getString(KEY_NTFY_TOPIC, DEFAULT_NTFY_TOPIC)
val cleanServer = server?.trimEnd('/') ?: DEFAULT_NTFY_SERVER.trimEnd('/')
val cleanTopic = topic?.trimStart('/') ?: DEFAULT_NTFY_TOPIC.trimStart('/')
return "$cleanServer/$cleanTopic"
}
// ----------------------------------------------------------------------
// 2. MAIN ACTIVITY CLASS
// ----------------------------------------------------------------------
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -37,133 +79,96 @@ class MainActivity : ComponentActivity() {
}
}
// ----------------------------------------------------------------------
// 3. MY APP (NAVIGATOR)
// ----------------------------------------------------------------------
sealed class ScreenState {
object Main : ScreenState()
object Settings : ScreenState()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyApp() {
val focusManager = LocalFocusManager.current
var currentScreen by remember { mutableStateOf<ScreenState>(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<String>()) }
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()
}