feat(ui): split MainActivity into ViewModel/UI/network modules; add checklist UI & 3D PanicButton
Split MainActivity into MainViewModel, MainScreen, PanicButton, NotificationSender, and ReportOption.\n\nReplaced dropdown with checklist, added emoji icons, notes field, and validation (catatan wajib when 'Lainnya' selected). Improves maintainability and testability. Ran editor static checks; no errors reported.
This commit is contained in:
parent
f0d6074b55
commit
b3c44df833
11
.idea/copilot.data.migration.agent.xml
generated
11
.idea/copilot.data.migration.agent.xml
generated
@ -16,6 +16,12 @@
|
||||
<option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" />
|
||||
<option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" />
|
||||
<option value="2b41e5bf-4d90-46d8-8fc8-7b41c3f788e1" />
|
||||
<option value="34b3c4dc-eb50-43b4-98d0-2b9771161619" />
|
||||
<option value="b32841ec-424b-46e4-af25-b31a5ca4ae51" />
|
||||
<option value="8589889d-a532-45c6-9ffe-0671c2d32583" />
|
||||
<option value="4f53b359-f3df-449a-ab3e-d112d5df446a" />
|
||||
<option value="e2907e39-fda1-4832-99ff-fb7407d64db3" />
|
||||
<option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
@ -25,6 +31,11 @@
|
||||
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89">
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt" />
|
||||
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/ReportOption.kt" />
|
||||
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt" />
|
||||
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/PanicButton.kt" />
|
||||
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainViewModel.kt" />
|
||||
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt" />
|
||||
</set>
|
||||
</entry>
|
||||
</pendingWorkingSetItems>
|
||||
|
||||
18
.idea/copilotDiffState.xml
generated
18
.idea/copilotDiffState.xml
generated
File diff suppressed because one or more lines are too long
@ -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<String?>(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<String, Boolean>().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 <your-topic> 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()
|
||||
}
|
||||
|
||||
100
app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt
Normal file
100
app/src/main/java/id/ac/ubharajaya/panicbutton/MainScreen.kt
Normal file
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<String?>(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
|
||||
}
|
||||
}
|
||||
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
package id.ac.ubharajaya.panicbutton
|
||||
|
||||
data class ReportOption(val label: String, val icon: String)
|
||||
Loading…
x
Reference in New Issue
Block a user