ci: fix issues ndas ku ngelu

This commit is contained in:
Rakha adi 2025-11-20 12:19:27 +07:00
parent 04315b8e58
commit 67ae1e6643
8 changed files with 534 additions and 149 deletions

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="AgentMigrationStateService"> <component name="AgentMigrationStateService">
<option name="migrationStatus" value="IN_PROGRESS" />
<option name="pendingSessionIds"> <option name="pendingSessionIds">
<option value="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" /> <option value="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" />
</option> </option>
@ -9,27 +10,37 @@
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89"> <entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89">
<value> <value>
<set> <set>
<option value="6dffb37e-ecb0-4c89-8cbe-e38ed347c397" /> <option value="0c019b23-843c-4710-8469-2dd1211d89a3" />
<option value="f607f271-4885-4c4e-91da-e2caf2abd67a" /> <option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" />
<option value="39a0d9c4-a68f-4050-bb14-eda17dc695db" /> <option value="2a8432cf-e7cb-4250-9c7c-00664ec22a28" />
<option value="94d90579-6a6d-44a9-8b93-de694c0c38ef" />
<option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" />
<option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" />
<option value="2b41e5bf-4d90-46d8-8fc8-7b41c3f788e1" /> <option value="2b41e5bf-4d90-46d8-8fc8-7b41c3f788e1" />
<option value="34b3c4dc-eb50-43b4-98d0-2b9771161619" /> <option value="34b3c4dc-eb50-43b4-98d0-2b9771161619" />
<option value="b32841ec-424b-46e4-af25-b31a5ca4ae51" /> <option value="38d23208-212e-48d5-84ea-03d6cf590d51" />
<option value="8589889d-a532-45c6-9ffe-0671c2d32583" /> <option value="39a0d9c4-a68f-4050-bb14-eda17dc695db" />
<option value="4f53b359-f3df-449a-ab3e-d112d5df446a" /> <option value="44cb5a83-abdc-41a2-95fa-283ec339fd2d" />
<option value="e2907e39-fda1-4832-99ff-fb7407d64db3" /> <option value="44d2412a-3471-4972-8b90-5cad4c5a00e3" />
<option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" />
<option value="461c1e05-de5d-4220-af67-2f29e576b9ea" /> <option value="461c1e05-de5d-4220-af67-2f29e576b9ea" />
<option value="9ebc3bf3-3f18-425d-a4fb-c87a6a121c9a" /> <option value="4f53b359-f3df-449a-ab3e-d112d5df446a" />
<option value="0c019b23-843c-4710-8469-2dd1211d89a3" /> <option value="520750f7-1625-4419-beb3-c4ecf7d9dc2b" />
<option value="8861f660-2aea-480d-a773-4e1f58fe9ac0" />
<option value="9523ff40-4e2b-4c96-b52e-69093f946698" />
<option value="80ef4e51-2808-48f4-9166-bf883578c6f0" />
<option value="2a8432cf-e7cb-4250-9c7c-00664ec22a28" />
<option value="52e099a9-0cd7-4716-8310-8a43635ca894" /> <option value="52e099a9-0cd7-4716-8310-8a43635ca894" />
<option value="6971fc05-6110-4b33-ac58-69628d336a48" />
<option value="6dffb37e-ecb0-4c89-8cbe-e38ed347c397" />
<option value="76858669-1b07-4bac-bc00-8448c673d06d" />
<option value="80ef4e51-2808-48f4-9166-bf883578c6f0" />
<option value="8589889d-a532-45c6-9ffe-0671c2d32583" />
<option value="8861f660-2aea-480d-a773-4e1f58fe9ac0" />
<option value="8fab86d7-b6d2-4ce8-9174-172bd49c7906" />
<option value="94d90579-6a6d-44a9-8b93-de694c0c38ef" />
<option value="9523ff40-4e2b-4c96-b52e-69093f946698" />
<option value="9ebc3bf3-3f18-425d-a4fb-c87a6a121c9a" />
<option value="aaf8bf1e-ce4a-48ab-a65e-6da5790fc566" />
<option value="b32841ec-424b-46e4-af25-b31a5ca4ae51" />
<option value="ba4f0602-d62b-4a9c-b86f-ee0dce40374d" />
<option value="c759a8ff-cf83-4618-90d4-31101dea7c77" />
<option value="e2907e39-fda1-4832-99ff-fb7407d64db3" />
<option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" />
<option value="f607f271-4885-4c4e-91da-e2caf2abd67a" />
<option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" />
</set> </set>
</value> </value>
</entry> </entry>
@ -38,13 +49,8 @@
<pendingWorkingSetItems> <pendingWorkingSetItems>
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89"> <entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89">
<set> <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/MainViewModel.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt" /> <option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/NotificationSender.kt" />
<option value="file://$PROJECT_DIR$/app/build.gradle.kts" />
</set> </set>
</entry> </entry>
</pendingWorkingSetItems> </pendingWorkingSetItems>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -60,5 +60,5 @@ dependencies {
//praktikum 1 //praktikum 1
implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("androidx.compose.material3:material3:1.2.0") implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.compose.material:material-icons-extended:<versi-compose>")
} }

View File

@ -1,145 +1,429 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card import androidx.compose.ui.draw.shadow
import androidx.compose.material3.CardDefaults import androidx.compose.ui.graphics.Brush
import androidx.compose.material3.Checkbox import androidx.compose.material3.*
import androidx.compose.material3.CheckboxDefaults import androidx.compose.runtime.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel) { fun MainScreen(viewModel: MainViewModel) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { bottomBar = {
PanicButton(onClick = { viewModel.sendAlert() }) PanicButton(onClick = { viewModel.sendAlert() })
} }
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.padding(16.dp), .padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Card(
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Jenis Kondisi Darurat",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp)) // Styled Emergency Condition Card
EmergencyConditionCard(viewModel)
viewModel.options.forEach { opt -> // Tambahkan spacer di luar Card agar ada jarak dari bottom bar
Row( Spacer(Modifier.height(20.dp))
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
) {
Box(
modifier = Modifier
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(text = opt.icon, fontSize = 22.sp)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = opt.label,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge
)
Checkbox(
checked = viewModel.isChecked(opt.label),
onCheckedChange = { isChecked -> viewModel.setChecked(opt.label, isChecked) },
colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colorScheme.primary)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = viewModel.otherNote,
onValueChange = { viewModel.otherNote = it },
label = { Text("Catatan tambahan (opsional)") },
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.dp, max = 120.dp),
colors = TextFieldDefaults.colors(
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
cursorColor = MaterialTheme.colorScheme.primary
)
)
if (viewModel.isChecked("Lainnya") && viewModel.otherNote.isBlank()) {
Text(
"Catatan wajib diisi jika memilih 'Lainnya'",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 4.dp, top = 4.dp)
)
}
}
}
} }
// Dialog Feedback
val dialogMessage = viewModel.dialogMessage val dialogMessage = viewModel.dialogMessage
if (!dialogMessage.isNullOrBlank()) { if (!dialogMessage.isNullOrBlank()) {
AlertDialog( AlertDialog(
onDismissRequest = { viewModel.clearDialog() }, onDismissRequest = { viewModel.clearDialog() },
confirmButton = { confirmButton = {
TextButton(onClick = { viewModel.clearDialog() }) { TextButton(onClick = { viewModel.clearDialog() }) {
Text("OK", color = MaterialTheme.colorScheme.primary) Text("OK")
} }
}, },
title = { Text("Notifikasi", style = MaterialTheme.typography.titleMedium) }, title = {
text = { Text(dialogMessage, style = MaterialTheme.typography.bodyMedium) }, Text(
"Notifikasi",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold
)
)
},
text = {
Text(
dialogMessage,
style = MaterialTheme.typography.bodyMedium
)
},
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface
) )
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmergencyConditionCard(viewModel: MainViewModel) {
var expanded by remember { mutableStateOf(false) }
Card(
shape = RoundedCornerShape(24.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(24.dp),
clip = true
)
) {
Column(
modifier = Modifier
.padding(24.dp)
) {
// Header dengan gradient background
Row(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
MaterialTheme.colorScheme.secondary.copy(alpha = 0.05f)
)
),
shape = RoundedCornerShape(16.dp)
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Icon dengan background
Box(
modifier = Modifier
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = "🚨",
style = MaterialTheme.typography.titleLarge
)
}
Column {
Text(
text = "Jenis Kondisi Darurat",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.primary
)
Text(
text = "Pilih satu atau lebih kondisi",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp)
)
}
}
Spacer(Modifier.height(24.dp))
// Custom Styled Dropdown
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
shape = RoundedCornerShape(16.dp)
)
.border(
width = 1.dp,
color = if (expanded)
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
shape = RoundedCornerShape(16.dp)
)
.clickable { expanded = !expanded }
.padding(horizontal = 18.dp, vertical = 16.dp)
) {
Column {
// Selected items preview
val selectedItems = viewModel.options.filter { viewModel.isChecked(it.label) }
if (selectedItems.isEmpty()) {
Text(
"Pilih kondisi darurat...",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
"${selectedItems.size} kondisi terpilih",
style = MaterialTheme.typography.bodySmall.copy(
fontWeight = FontWeight.Medium
),
color = MaterialTheme.colorScheme.primary
)
// Show first 2 selected items as chips
selectedItems.take(2).forEach { option ->
SelectedChip(option = option, viewModel = viewModel)
}
if (selectedItems.size > 2) {
Text(
"+${selectedItems.size - 2} lainnya",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
// Dropdown Menu
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(IntrinsicSize.Max)
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp)
)
) {
Column(
modifier = Modifier
.width(320.dp)
.padding(vertical = 8.dp)
.heightIn(max = 280.dp)
.verticalScroll(rememberScrollState())
) {
viewModel.options.forEach { option ->
EmergencyOptionItem(
option = option,
isChecked = viewModel.isChecked(option.label),
onCheckedChange = { checked ->
viewModel.setChecked(option.label, checked)
}
)
}
}
}
}
// Dropdown arrow
Icon(
imageVector = if (expanded)
Icons.Filled.KeyboardArrowUp
else
Icons.Filled.KeyboardArrowDown,
contentDescription = if (expanded) "Tutup" else "Buka",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.align(Alignment.CenterEnd)
)
}
Spacer(Modifier.height(20.dp))
// Extra note input dengan styling yang lebih baik
Column {
Text(
"Catatan Tambahan",
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium
),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 8.dp, start = 4.dp)
)
OutlinedTextField(
value = viewModel.otherNote,
onValueChange = { viewModel.otherNote = it },
placeholder = {
Text(
"Tambahkan catatan jika diperlukan...",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.dp, max = 120.dp),
shape = RoundedCornerShape(14.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f),
cursorColor = MaterialTheme.colorScheme.primary,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
unfocusedTextColor = MaterialTheme.colorScheme.onSurface
),
textStyle = MaterialTheme.typography.bodyMedium
)
// Validation jika pilih 'Lainnya' tapi kosong
if (viewModel.isChecked("Lainnya") && viewModel.otherNote.isBlank()) {
Row(
modifier = Modifier.padding(top = 6.dp, start = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Warning",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
Text(
"Catatan wajib diisi jika memilih 'Lainnya'",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}
@Composable
fun EmergencyOptionItem(
option: EmergencyOption,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onCheckedChange(!isChecked) }
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
// Icon dengan background
Box(
modifier = Modifier
.size(36.dp)
.background(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = option.icon,
style = MaterialTheme.typography.bodyLarge
)
}
Spacer(Modifier.width(12.dp))
// Label
Text(
text = option.label,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = if (isChecked) FontWeight.Medium else FontWeight.Normal
),
color = if (isChecked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(8.dp))
// Custom Checkbox
Box(
modifier = Modifier
.size(20.dp)
.background(
color = if (isChecked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(4.dp)
)
.border(
width = 1.dp,
color = if (isChecked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.outline.copy(alpha = 0.6f),
shape = RoundedCornerShape(4.dp)
),
contentAlignment = Alignment.Center
) {
if (isChecked) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(14.dp)
)
}
}
}
}
@Composable
fun SelectedChip(option: EmergencyOption, viewModel: MainViewModel) {
Box(
modifier = Modifier
.wrapContentWidth()
.background(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${option.icon} ${option.label}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
maxLines = 1
)
Spacer(Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Hapus",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(14.dp)
.clickable { viewModel.setChecked(option.label, false) }
)
}
}
}

View File

@ -1,18 +1,25 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import androidx.compose.runtime.getValue import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
// Ganti ReportOption dengan EmergencyOption
data class EmergencyOption(
val label: String,
val icon: String
)
class MainViewModel : ViewModel() { class MainViewModel : ViewModel() {
val options = listOf( val options = listOf(
ReportOption("Kebakaran", "🔥"), EmergencyOption("Kebakaran", "🔥"),
ReportOption("Banjir", "🌊"), EmergencyOption("Banjir", "🌊"),
ReportOption("Gempa Bumi", "🌋"), EmergencyOption("Gempa Bumi", "🌍"),
ReportOption("Huru Hara/Demonstrasi", "💥"), EmergencyOption("Huru Hara/Demonstrasi", "💥"),
ReportOption("Lainnya", "✏️") EmergencyOption("Lainnya", "✏️")
) )
// observable map so Compose recomposes on changes // observable map so Compose recomposes on changes
@ -27,6 +34,10 @@ class MainViewModel : ViewModel() {
fun setChecked(label: String, isChecked: Boolean) { fun setChecked(label: String, isChecked: Boolean) {
_checkedState[label] = isChecked _checkedState[label] = isChecked
// Clear otherNote jika "Lainnya" di-uncheck
if (label == "Lainnya" && !isChecked) {
otherNote = ""
}
} }
fun sendAlert() { fun sendAlert() {
@ -42,11 +53,27 @@ class MainViewModel : ViewModel() {
return return
} }
// Simulate sending data // Build message payload
dialogMessage = "Laporan terkirim!\nKondisi: ${selectedOptions.joinToString() }" val bodyBuilder = StringBuilder()
bodyBuilder.append("Kondisi: ${selectedOptions.joinToString(", ")}")
if (otherNote.isNotBlank()) {
bodyBuilder.append("\nCatatan: ${otherNote.trim()}")
}
val payload = bodyBuilder.toString()
// Use NotificationSender utility to send the payload
viewModelScope.launch {
try {
val resultMessage = NotificationSender.sendNotification(payload)
dialogMessage = resultMessage
} catch (e: Exception) {
e.printStackTrace()
dialogMessage = "Gagal mengirim laporan. Silakan periksa koneksi Anda dan coba lagi."
}
}
} }
fun clearDialog() { fun clearDialog() {
dialogMessage = null dialogMessage = null
} }
} }

View File

@ -1,29 +1,79 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import android.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
object NotificationSender { object NotificationSender {
private val client = OkHttpClient() private val client = OkHttpClient.Builder()
.build()
private const val url = "https://ntfy.ubharajaya.ac.id/panic-button" private const val url = "https://ntfy.ubharajaya.ac.id/panic-button"
private const val TAG = "NotificationSender"
// Header MUST be ASCII only
private fun sanitizeHeaderValue(value: String): String {
val sb = StringBuilder()
for (ch in value) {
val code = ch.code
if (code in 32..126) sb.append(ch)
}
val out = sb.toString()
return if (out.isBlank()) "Alert" else out
}
suspend fun sendNotification(message: String): String = withContext(Dispatchers.IO) { suspend fun sendNotification(message: String): String = withContext(Dispatchers.IO) {
val body = message.toRequestBody("text/plain".toMediaType()) try {
val request = Request.Builder() Log.d(TAG, "Preparing notification to $url")
.url(url)
.addHeader("Title", "Alert")
.addHeader("Priority", "urgent")
.addHeader("Tags", "alert warning,rotating_light")
.post(body)
.build()
client.newCall(request).execute().use { resp -> // Title ONLY has emoji
if (resp.isSuccessful) "Notifikasi berhasil dikirim!" else "Gagal mengirim notifikasi: ${resp.code}" val titleSafe = sanitizeHeaderValue("🚨 Alert 🚨")
// Body is clean text, no emoji auto-append
val cleanMessage = message.trim()
val body = cleanMessage.toRequestBody("text/plain".toMediaType())
val request = Request.Builder()
.url(url)
.addHeader("Title", titleSafe)
.addHeader("Priority", "urgent")
.addHeader("Tags", "warning")
.post(body)
.build()
// Debug logging
Log.d(TAG, "Body to send: $cleanMessage")
client.newCall(request).execute().use { resp ->
val respCode = resp.code
val respMessage = resp.message
val respBody = try {
resp.body?.string()
} catch (e: Exception) {
null
}
Log.d(TAG, "Response: code=$respCode message=$respMessage body=$respBody")
return@withContext if (resp.isSuccessful) {
"Notifikasi berhasil dikirim!"
} else {
"Gagal mengirim notifikasi: $respCode $respMessage ${respBody ?: ""}".trim()
}
}
} catch (e: IOException) {
Log.e(TAG, "IO error", e)
return@withContext "Gagal mengirim notifikasi: ${e.localizedMessage}"
} catch (e: Exception) {
Log.e(TAG, "Unexpected error", e)
return@withContext "Gagal mengirim notifikasi: ${e.localizedMessage}"
} }
} }
} }