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"?>
|
<?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
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
|
//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>")
|
||||||
}
|
}
|
||||||
@ -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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user