ci: fix issues ndas ku ngelu
This commit is contained in:
parent
04315b8e58
commit
67ae1e6643
50
.idea/copilot.data.migration.agent.xml
generated
50
.idea/copilot.data.migration.agent.xml
generated
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="IN_PROGRESS" />
|
||||
<option name="pendingSessionIds">
|
||||
<option value="bf5061b4-a1e9-4600-a273-ea8e51b2ce89" />
|
||||
</option>
|
||||
@ -9,27 +10,37 @@
|
||||
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89">
|
||||
<value>
|
||||
<set>
|
||||
<option value="6dffb37e-ecb0-4c89-8cbe-e38ed347c397" />
|
||||
<option value="f607f271-4885-4c4e-91da-e2caf2abd67a" />
|
||||
<option value="39a0d9c4-a68f-4050-bb14-eda17dc695db" />
|
||||
<option value="94d90579-6a6d-44a9-8b93-de694c0c38ef" />
|
||||
<option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" />
|
||||
<option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" />
|
||||
<option value="0c019b23-843c-4710-8469-2dd1211d89a3" />
|
||||
<option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" />
|
||||
<option value="2a8432cf-e7cb-4250-9c7c-00664ec22a28" />
|
||||
<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" />
|
||||
<option value="38d23208-212e-48d5-84ea-03d6cf590d51" />
|
||||
<option value="39a0d9c4-a68f-4050-bb14-eda17dc695db" />
|
||||
<option value="44cb5a83-abdc-41a2-95fa-283ec339fd2d" />
|
||||
<option value="44d2412a-3471-4972-8b90-5cad4c5a00e3" />
|
||||
<option value="461c1e05-de5d-4220-af67-2f29e576b9ea" />
|
||||
<option value="9ebc3bf3-3f18-425d-a4fb-c87a6a121c9a" />
|
||||
<option value="0c019b23-843c-4710-8469-2dd1211d89a3" />
|
||||
<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="4f53b359-f3df-449a-ab3e-d112d5df446a" />
|
||||
<option value="520750f7-1625-4419-beb3-c4ecf7d9dc2b" />
|
||||
<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>
|
||||
</value>
|
||||
</entry>
|
||||
@ -38,13 +49,8 @@
|
||||
<pendingWorkingSetItems>
|
||||
<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" />
|
||||
<option value="file://$PROJECT_DIR$/app/build.gradle.kts" />
|
||||
</set>
|
||||
</entry>
|
||||
</pendingWorkingSetItems>
|
||||
|
||||
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/copilot.data.migration.ask.xml
generated
Normal 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>
|
||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal 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
6
.idea/copilot.data.migration.edit.xml
generated
Normal 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>
|
||||
@ -60,5 +60,5 @@ dependencies {
|
||||
//praktikum 1
|
||||
implementation("com.squareup.okhttp3:okhttp:4.11.0")
|
||||
implementation("androidx.compose.material3:material3:1.2.0")
|
||||
|
||||
implementation("androidx.compose.material:material-icons-extended:<versi-compose>")
|
||||
}
|
||||
@ -1,145 +1,429 @@
|
||||
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.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
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.foundation.verticalScroll
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(viewModel: MainViewModel) {
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
PanicButton(onClick = { viewModel.sendAlert() })
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
.padding(20.dp),
|
||||
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 ->
|
||||
Row(
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tambahkan spacer di luar Card agar ada jarak dari bottom bar
|
||||
Spacer(Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
// Dialog Feedback
|
||||
val dialogMessage = viewModel.dialogMessage
|
||||
if (!dialogMessage.isNullOrBlank()) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.clearDialog() },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.clearDialog() }) {
|
||||
Text("OK", color = MaterialTheme.colorScheme.primary)
|
||||
Text("OK")
|
||||
}
|
||||
},
|
||||
title = { Text("Notifikasi", style = MaterialTheme.typography.titleMedium) },
|
||||
text = { Text(dialogMessage, style = MaterialTheme.typography.bodyMedium) },
|
||||
title = {
|
||||
Text(
|
||||
"Notifikasi",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
dialogMessage,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,25 @@
|
||||
package id.ac.ubharajaya.panicbutton
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
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() {
|
||||
val options = listOf(
|
||||
ReportOption("Kebakaran", "🔥"),
|
||||
ReportOption("Banjir", "🌊"),
|
||||
ReportOption("Gempa Bumi", "🌋"),
|
||||
ReportOption("Huru Hara/Demonstrasi", "💥"),
|
||||
ReportOption("Lainnya", "✏️")
|
||||
EmergencyOption("Kebakaran", "🔥"),
|
||||
EmergencyOption("Banjir", "🌊"),
|
||||
EmergencyOption("Gempa Bumi", "🌍"),
|
||||
EmergencyOption("Huru Hara/Demonstrasi", "💥"),
|
||||
EmergencyOption("Lainnya", "✏️")
|
||||
)
|
||||
|
||||
// observable map so Compose recomposes on changes
|
||||
@ -27,6 +34,10 @@ class MainViewModel : ViewModel() {
|
||||
|
||||
fun setChecked(label: String, isChecked: Boolean) {
|
||||
_checkedState[label] = isChecked
|
||||
// Clear otherNote jika "Lainnya" di-uncheck
|
||||
if (label == "Lainnya" && !isChecked) {
|
||||
otherNote = ""
|
||||
}
|
||||
}
|
||||
|
||||
fun sendAlert() {
|
||||
@ -42,8 +53,24 @@ class MainViewModel : ViewModel() {
|
||||
return
|
||||
}
|
||||
|
||||
// Simulate sending data
|
||||
dialogMessage = "Laporan terkirim!\nKondisi: ${selectedOptions.joinToString() }"
|
||||
// Build message payload
|
||||
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() {
|
||||
|
||||
@ -1,29 +1,79 @@
|
||||
package id.ac.ubharajaya.panicbutton
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.IOException
|
||||
|
||||
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 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) {
|
||||
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()
|
||||
try {
|
||||
Log.d(TAG, "Preparing notification to $url")
|
||||
|
||||
client.newCall(request).execute().use { resp ->
|
||||
if (resp.isSuccessful) "Notifikasi berhasil dikirim!" else "Gagal mengirim notifikasi: ${resp.code}"
|
||||
// Title ONLY has emoji
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user