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:
parent
9807c900dd
commit
e2bd34be8a
@ -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")
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user