Perbaikan UI/UX

This commit is contained in:
202310715320 AHMAR RAFLY MARYADI 2026-01-12 16:11:41 +07:00
parent 2365e0b907
commit 214af2312f
2 changed files with 299 additions and 143 deletions

View File

@ -26,6 +26,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
@ -103,6 +104,15 @@ data class AssessmentEntry(
val dateString: String = ""
)
data class CognitiveTestEntry(
val id: String = "",
val userId: String = "",
val testType: String = "",
val score: Int = 0,
val timestamp: com.google.firebase.Timestamp? = null,
val dateString: String = ""
)
data class Badge(
val title: String,
val description: String,
@ -280,20 +290,27 @@ fun calculateDailyStreak(dates: List<Date>): Int {
return streak
}
fun getBadges(journals: List<JournalEntry>, assessments: List<AssessmentEntry>, streak: Int): List<Badge> {
val totalActivities = journals.size + assessments.size
fun getBadges(journals: List<JournalEntry>, assessments: List<AssessmentEntry>, cognitiveTests: List<CognitiveTestEntry>, streak: Int): List<Badge> {
val totalActivities = journals.size + assessments.size + cognitiveTests.size
val reflectionEntries = journals.count { it.type == "reflection" }
// Untuk demo, kita ambil tanggal hari ini jika lencana terbuka
val today = SimpleDateFormat("d MMM yyyy", Locale("id", "ID")).format(Date())
val memoryTests = cognitiveTests.count { it.testType == "memory" }
val focusTests = cognitiveTests.count { it.testType == "focus" }
val reactionTests = cognitiveTests.count { it.testType == "reaction" }
val logicalTests = cognitiveTests.count { it.testType == "logical" }
return listOf(
Badge("Langkah Awal", "Lakukan aktivitas pertamamu untuk memulai perjalanan kesehatan mental.", Icons.Default.DirectionsRun, totalActivities >= 1, if(totalActivities >= 1) today else null),
Badge("Penulis Rutin", "Berhasil menulis 5 jurnal bebas sebagai bentuk ekspresi diri.", Icons.Default.EditNote, journals.size >= 5, if(journals.size >= 5) today else null),
Badge("Jiwa Reflektif", "Selesaikan 3 refleksi diri untuk mengenal dirimu lebih dalam.", Icons.Default.SelfImprovement, reflectionEntries >= 3, if(reflectionEntries >= 3) today else null),
Badge("Disiplin Diri", "Pertahankan aktivitas selama 7 hari tanpa terputus.", Icons.Default.LocalFireDepartment, streak >= 7, if(streak >= 7) today else null),
Badge("Pencari Jawaban", "Lakukan 5 penilaian harian untuk memantau kondisi mentalmu.", Icons.Default.Psychology, assessments.size >= 5, if(assessments.size >= 5) today else null),
Badge("Master Fokus", "Selesaikan total 10 aktivitas apa saja di dalam aplikasi.", Icons.Default.EmojiEvents, totalActivities >= 10, if(totalActivities >= 10) today else null)
Badge("Master Fokus", "Selesaikan total 10 aktivitas apa saja di dalam aplikasi.", Icons.Default.EmojiEvents, totalActivities >= 10, if(totalActivities >= 10) today else null),
Badge("Ingatan Gajah", "Selesaikan 3 tes memori.", Icons.Default.Memory, memoryTests >= 3, if(memoryTests >= 3) today else null),
Badge("Mata Elang", "Selesaikan 3 tes fokus.", Icons.Default.Visibility, focusTests >= 3, if(focusTests >= 3) today else null),
Badge("Kilat Visual", "Selesaikan 3 tes kecepatan reaksi.", Icons.Default.Bolt, reactionTests >= 3, if(reactionTests >= 3) today else null),
Badge("Logika Tajam", "Selesaikan 3 tes logika.", Icons.Default.Lightbulb, logicalTests >= 3, if(logicalTests >= 3) today else null)
)
}
@ -303,27 +320,34 @@ fun ProfileScreen(navController: NavController) {
val auth = Firebase.auth
val user = auth.currentUser
val db = Firebase.firestore
val context = LocalContext.current
var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
var assessmentList by remember { mutableStateOf<List<AssessmentEntry>>(emptyList()) }
var cognitiveTestList by remember { mutableStateOf<List<CognitiveTestEntry>>(emptyList()) }
var dailyStreak by remember { mutableIntStateOf(0) }
var selectedBadge by remember { mutableStateOf<Badge?>(null) }
var expanded by remember { mutableStateOf(false) }
var selectedReport by remember { mutableStateOf("Pilih Laporan") }
val reportOptions = listOf("Mingguan", "Bulanan", "Tahunan")
val scope = rememberCoroutineScope()
LaunchedEffect(user) {
if (user != null) {
scope.launch {
val journalTask = db.collection("journals").whereEqualTo("userId", user.uid).get()
val assessmentTask = db.collection("assessments").whereEqualTo("userId", user.uid).get()
try {
val journalSnap = journalTask.await()
val assessmentSnap = assessmentTask.await()
val journalSnap = db.collection("journals").whereEqualTo("userId", user.uid).get().await()
val assessmentSnap = db.collection("assessments").whereEqualTo("userId", user.uid).get().await()
val cognitiveSnap = db.collection("cognitive_tests").whereEqualTo("userId", user.uid).get().await()
journalList = journalSnap.toObjects(JournalEntry::class.java)
assessmentList = assessmentSnap.toObjects(AssessmentEntry::class.java)
cognitiveTestList = cognitiveSnap.toObjects(CognitiveTestEntry::class.java)
val allDates = (journalList.mapNotNull { it.timestamp?.toDate() } +
assessmentList.mapNotNull { it.timestamp?.toDate() })
assessmentList.mapNotNull { it.timestamp?.toDate() } +
cognitiveTestList.mapNotNull { it.timestamp?.toDate() })
dailyStreak = calculateDailyStreak(allDates)
} catch (e: Exception) {
@ -333,84 +357,162 @@ fun ProfileScreen(navController: NavController) {
}
}
val badges = remember(journalList, assessmentList, dailyStreak) {
getBadges(journalList, assessmentList, dailyStreak)
val badges = remember(journalList, assessmentList, cognitiveTestList, dailyStreak) {
getBadges(journalList, assessmentList, cognitiveTestList, dailyStreak)
}
if (selectedBadge != null) {
BadgeDetailDialog(badge = selectedBadge!!, onDismiss = { selectedBadge = null })
}
LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {
item { if (user != null) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage(model = user.photoUrl, contentDescription = "Profile Picture", modifier = Modifier.size(64.dp).clip(CircleShape)); Spacer(modifier = Modifier.width(16.dp)); Column { Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall); Text(user.email ?: "Tamu", style = MaterialTheme.typography.bodyMedium) } } } else { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.AccountCircle, null, modifier = Modifier.size(64.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("Mode Tamu", style = MaterialTheme.typography.headlineSmall); Text("Masuk untuk simpan data", style = MaterialTheme.typography.bodyMedium) } } } }
item { Card(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = Color(0xFFFFA500), modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium); Text("Streak Aktivitas Harian", style = MaterialTheme.typography.bodySmall) } } } }
item {
Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp))
}
item {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.height(240.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(badges) { badge ->
BadgeItem(badge = badge, onClick = { selectedBadge = badge })
Box(modifier = Modifier.fillMaxSize()) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
contentPadding = PaddingValues(top = 16.dp, bottom = 80.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// User Info
item(span = { GridItemSpan(3) }) {
if (user != null) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) {
AsyncImage(model = user.photoUrl, contentDescription = null, modifier = Modifier.size(64.dp).clip(CircleShape))
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall)
Text(user.email ?: "Tamu", style = MaterialTheme.typography.bodyMedium)
}
}
}
}
// Streak Card
item(span = { GridItemSpan(3) }) {
Card(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.LocalFireDepartment, null, tint = Color(0xFFFFA500), modifier = Modifier.size(40.dp))
Spacer(modifier = Modifier.width(16.dp))
Column {
Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium)
Text("Streak Aktivitas Harian", style = MaterialTheme.typography.bodySmall)
}
}
}
}
// Badge Header
item(span = { GridItemSpan(3) }) {
Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
}
// Badges Grid items
items(badges) { badge ->
BadgeItem(badge = badge) {
selectedBadge = badge
}
}
// Report Header
item(span = { GridItemSpan(3) }) {
Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(top = 16.dp))
}
// Report Dropdown
item(span = { GridItemSpan(3) }) {
Box(modifier = Modifier.fillMaxWidth()) {
OutlinedCard(
modifier = Modifier.fillMaxWidth().clickable { expanded = true },
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(selectedReport, fontWeight = FontWeight.SemiBold)
Icon(if (expanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, contentDescription = null)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.fillMaxWidth(0.9f)
) {
reportOptions.forEach { option ->
val description = when(option) {
"Mingguan" -> "Analisis 7 hari terakhir"
"Bulanan" -> "Analisis 30 hari terakhir"
else -> "Tersedia di akhir tahun."
}
DropdownMenuItem(
text = {
Column {
Text(option, fontWeight = FontWeight.Bold)
Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray)
}
},
onClick = {
if (option != "Tahunan") {
selectedReport = option
expanded = false
} else {
Toast.makeText(context, "Laporan tahunan tersedia di akhir tahun", Toast.LENGTH_SHORT).show()
}
}
)
}
}
}
}
// History Button
item(span = { GridItemSpan(3) }) {
OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) {
Text("Lihat Riwayat Jurnal Lengkap")
}
}
}
item {
Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ReportCard("Mingguan", "Analisis 7 hari", Modifier.weight(1f))
ReportCard("Bulanan", "Analisis 30 hari", Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(8.dp))
ReportCard("Tahunan", "Tersedia 15 Des", Modifier.fillMaxWidth(), enabled = false)
if (selectedBadge != null) {
BadgeDetailDialog(badge = selectedBadge!!, onDismiss = { selectedBadge = null })
}
item { OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth()) { Text("Lihat Riwayat Jurnal Lengkap") } }
}
}
@Composable
fun BadgeItem(badge: Badge, onClick: () -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.alpha(if (badge.isUnlocked) 1f else 0.4f)
fun BadgeItem(badge: Badge, modifier: Modifier = Modifier, onClick: () -> Unit) {
Surface(
onClick = onClick,
modifier = modifier.alpha(if (badge.isUnlocked) 1f else 0.4f),
color = Color.Transparent,
shape = RoundedCornerShape(12.dp)
) {
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f))
.border(2.dp, if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.5f), CircleShape),
contentAlignment = Alignment.Center
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth()
) {
Icon(
badge.icon,
contentDescription = badge.title,
modifier = Modifier.size(32.dp),
tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f))
.border(2.dp, if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.5f), CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
badge.icon,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
badge.title,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
lineHeight = 14.sp
)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
badge.title,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
lineHeight = 14.sp
)
}
}
@ -418,29 +520,32 @@ fun BadgeItem(badge: Badge, onClick: () -> Unit) {
fun BadgeDetailDialog(badge: Badge, onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(
shape = RoundedCornerShape(24.dp),
modifier = Modifier.fillMaxWidth().padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
shape = RoundedCornerShape(28.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier.padding(24.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, contentDescription = "Close") }
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, null, tint = Color.White) }
IconButton(onClick = { }) { Icon(Icons.Default.Share, null, tint = Color.White) }
}
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f)),
.background(if (badge.isUnlocked) MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) else Color.DarkGray),
contentAlignment = Alignment.Center
) {
Icon(
badge.icon,
contentDescription = badge.title,
modifier = Modifier.size(60.dp),
contentDescription = null,
modifier = Modifier.size(70.dp),
tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
)
}
@ -453,87 +558,56 @@ fun BadgeDetailDialog(badge: Badge, onDismiss: () -> Unit) {
shape = RoundedCornerShape(8.dp)
) {
Text(
text = badge.dateUnlocked,
color = Color.White,
text = badge.dateUnlocked.uppercase(),
color = Color(0xFFFFA500),
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
fontWeight = FontWeight.Bold
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
fontWeight = FontWeight.ExtraBold
)
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(20.dp))
}
Text(
text = if (badge.isUnlocked) "Kamu meraih pencapaian" else "Pencapaian Terkunci",
color = Color.LightGray,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = badge.title,
color = Color.White,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = badge.description,
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
lineHeight = 20.sp
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(50)
modifier = Modifier.fillMaxWidth().height(50.dp),
shape = RoundedCornerShape(25.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Text("Tutup")
Text("Tutup", fontWeight = FontWeight.Bold)
}
}
}
}
}
@Composable
fun ReportCard(title: String, subtitle: String, modifier: Modifier = Modifier, enabled: Boolean = true) {
Card(
onClick = { /* Navigasi ke laporan terkait */ },
enabled = enabled,
modifier = modifier,
colors = if (enabled) CardDefaults.cardColors() else CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Column(Modifier.padding(12.dp)) {
Text(title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleSmall)
Text(subtitle, style = MaterialTheme.typography.labelSmall, color = if(enabled) MaterialTheme.colorScheme.onSurfaceVariant else Color.Gray)
}
}
}
fun scheduleReminder(context: Context, hour: Int, minute: Int) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, MyReminderReceiver::class.java).apply { putExtra("title", "Waktunya Jurnaling!"); putExtra("message", "Luangkan waktu sejenak untuk refleksi diri.") }
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val calendar = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, hour); set(Calendar.MINUTE, minute); set(Calendar.SECOND, 0); if (before(Calendar.getInstance())) add(Calendar.DATE, 1) }
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if(alarmManager.canScheduleExactAlarms()){
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent)
}
} else {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent)
}
Toast.makeText(context, "Pengingat diatur!", Toast.LENGTH_SHORT).show()
} catch (e: SecurityException) {
Toast.makeText(context, "Izin alarm tidak diberikan", Toast.LENGTH_SHORT).show()
}
}
fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, indicators: Map<String, Float>): Int {
var baseScore = 30.0
when (sentimentLabel) { "sadness", "fear", "anger" -> baseScore += (sentimentScore * 10); "joy" -> baseScore -= (sentimentScore * 10) }
@ -547,8 +621,8 @@ fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, in
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JournalScreen() {
var selectedTab by remember { mutableIntStateOf(0) }; var journalText by remember { mutableStateOf("") }; var reflectionAnswers by remember { mutableStateOf(List(3) { "" }) }; var isSaving by remember { mutableStateOf(false) }; var detectedEmotion by remember { mutableStateOf<String?>(null) }; var detectedIssues by remember { mutableStateOf<String?>(null) }; var calculatedScoreFeedback by remember { mutableIntStateOf(-1) }; var isCriticalRisk by remember { mutableStateOf(false) }; val context = LocalContext.current; val auth = Firebase.auth; val db = Firebase.firestore; val scope = rememberCoroutineScope(); val reflectionQuestions = listOf("Satu hal kecil yang membuatmu tersenyum hari ini?", "Tantangan terbesar hari ini & solusinya?", "Satu hal yang ingin kamu perbaiki besok?"); val calendar = Calendar.getInstance(); val timePickerDialog = TimePickerDialog(context, { _, hour, minute -> scheduleReminder(context, hour, minute) }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); val depressionIndicators = listOf("suasana hati sedih", "kehilangan minat", "mudah marah", "perasaan bersalah", "perasaan tidak berharga", "sulit berkonsentrasi", "sulit mengambil keputusan", "pesimis", "menyalahkan diri sendiri", "kehilangan energi", "penurunan aktivitas", "menarik diri sosial", "gangguan tidur", "perubahan nafsu makan", "perubahan berat badan", "pikiran tentang kematian", "keinginan untuk mati", "pikiran bunuh diri", "rencana bunuh diri")
Scaffold(topBar = { TopAppBar(title = { Text("Jurnal & Refleksi") }, actions = { IconButton(onClick = { timePickerDialog.show() }) { Icon(Icons.Default.Alarm, contentDescription = "Set Reminder") } }) }) { padding ->
var selectedTab by remember { mutableIntStateOf(0) }; var journalText by remember { mutableStateOf("") }; var reflectionAnswers by remember { mutableStateOf(List(3) { "" }) }; var isSaving by remember { mutableStateOf(false) }; var detectedEmotion by remember { mutableStateOf<String?>(null) }; var detectedIssues by remember { mutableStateOf<String?>(null) }; var calculatedScoreFeedback by remember { mutableIntStateOf(-1) }; var isCriticalRisk by remember { mutableStateOf(false) }; val context = LocalContext.current; val auth = Firebase.auth; val db = Firebase.firestore; val scope = rememberCoroutineScope(); val depressionIndicators = listOf("suasana hati sedih", "kehilangan minat", "mudah marah", "perasaan bersalah", "perasaan tidak berharga", "sulit berkonsentrasi", "sulit mengambil keputusan", "pesimis", "menyalahkan diri sendiri", "kehilangan energi", "penurunan aktivitas", "menarik diri sosial", "gangguan tidur", "perubahan nafsu makan", "perubahan berat badan", "pikiran tentang kematian", "keinginan untuk mati", "pikiran bunuh diri", "rencana bunuh diri"); val reflectionQuestions = listOf("Satu hal kecil yang membuatmu tersenyum hari ini?", "Tantangan terbesar hari ini & solusinya?", "Satu hal yang ingin kamu perbaiki besok?")
Scaffold(topBar = { TopAppBar(title = { Text("Jurnal & Refleksi") }) }) { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
TabRow(selectedTabIndex = selectedTab) { Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }, text = { Text("Jurnal Bebas") }); Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }, text = { Text("Refleksi") }) }
Spacer(modifier = Modifier.height(16.dp))
@ -568,7 +642,28 @@ fun JournalScreen() {
}
}
@OptIn(ExperimentalMaterial3Api::class) @Composable fun AssessmentScreen() {
fun getAssessmentColor(score: Int): Color {
return when {
score <= 4 -> Color(0xFF4CAF50) // Green (Normal)
score <= 9 -> Color(0xFFFFEB3B) // Yellow (Ringan)
score <= 14 -> Color(0xFFFF9800) // Orange (Sedang)
else -> Color(0xFFF44336) // Red (Berat)
}
}
fun getIndicatorColor(value: Float): Color {
return when (value.toInt()) {
0 -> Color(0xFF4CAF50) // Green
1 -> Color(0xFFFFEB3B) // Yellow
2 -> Color(0xFFFF9800) // Orange
3 -> Color(0xFFF44336) // Red
else -> Color.Gray
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AssessmentScreen() {
val indicators = remember { listOf("Mood Sedih", "Rasa Bersalah", "Menarik Diri", "Sulit Konsentrasi", "Lelah", "Pikiran Bunuh Diri") }
val sliderValues = remember { mutableStateMapOf<String, Float>().apply { indicators.forEach { put(it, 0f) } } }
val totalScore = sliderValues.values.sum().toInt()
@ -577,11 +672,27 @@ fun JournalScreen() {
val db = Firebase.firestore
val context = LocalContext.current
var isSaving by remember { mutableStateOf(false) }
val summaryColor = getAssessmentColor(totalScore)
Scaffold(topBar = { TopAppBar(title = { Text("Penilaian Harian") }) }) { innerPadding ->
LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
item { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) { Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium); Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium); Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) } } }
items(indicators) { indicator -> IndicatorItem(indicatorName = indicator, value = sliderValues[indicator] ?: 0f) { sliderValues[indicator] = it.roundToInt().toFloat() } }
item {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = summaryColor.copy(alpha = 0.2f))
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium)
Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium)
Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
}
}
}
items(indicators) { indicator ->
IndicatorItem(indicatorName = indicator, value = sliderValues[indicator] ?: 0f) {
sliderValues[indicator] = it.roundToInt().toFloat()
}
}
item {
Button(
onClick = {
@ -609,31 +720,76 @@ fun JournalScreen() {
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isSaving
enabled = !isSaving,
colors = ButtonDefaults.buttonColors(containerColor = summaryColor)
) {
if (isSaving) CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White)
else Text("Selesai & Simpan")
else Text("Selesai & Simpan", color = if (totalScore > 4 && totalScore <= 14) Color.Black else Color.White)
}
}
}
}
}
@Composable fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) { val description = when (value.toInt()) { 0 -> "Tidak sama sekali"; 1 -> "Beberapa hari"; 2 -> "Separuh hari"; 3 -> "Hampir setiap hari"; else -> "" }; Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(indicatorName, style = MaterialTheme.typography.titleMedium); Text(text = value.toInt().toString(), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) }; Slider(value = value, onValueChange = onValueChange, valueRange = 0f..3f, steps = 2); Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally)) } } }
@Composable
fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) {
val description = when (value.toInt()) {
0 -> "Tidak sama sekali"
1 -> "Beberapa hari"
2 -> "Separuh hari"
3 -> "Hampir setiap hari"
else -> ""
}
val indicatorColor = getIndicatorColor(value)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(indicatorName, style = MaterialTheme.typography.titleMedium)
Text(text = value.toInt().toString(), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
}
Slider(
value = value,
onValueChange = onValueChange,
valueRange = 0f..3f,
steps = 2,
colors = SliderDefaults.colors(
thumbColor = indicatorColor,
activeTrackColor = indicatorColor,
inactiveTrackColor = indicatorColor.copy(alpha = 0.24f)
)
)
Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally))
}
}
}
fun saveCognitiveResult(userId: String, testType: String, score: Int) {
val db = Firebase.firestore
val entry = hashMapOf(
"userId" to userId,
"testType" to testType,
"score" to score,
"timestamp" to FieldValue.serverTimestamp(),
"dateString" to SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date())
)
db.collection("cognitive_tests").add(entry)
}
@OptIn(ExperimentalMaterial3Api::class) @Composable fun CognitiveTestScreen(navController: NavController) { Column(modifier = Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Pilih Tes Kognitif", style = MaterialTheme.typography.headlineSmall); TestCard("Tes Memori", "Uji memori jangka pendek", Icons.Default.Memory, "memory_test", navController); TestCard("Tes Fokus", "Uji fokus & atensi", Icons.Default.CenterFocusStrong, "focus_test", navController); TestCard("Tes Kecepatan Reaksi", "Uji refleks visual", Icons.Default.Speed, "reaction_test", navController); TestCard("Tes Logika", "Uji pola pikir & logika", Icons.Default.Psychology, "logical_test", navController) } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun TestCard(title: String, description: String, icon: ImageVector, route: String, navController: NavController) { Card(onClick = { navController.navigate(route) }, modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, modifier = Modifier.size(40.dp)); Spacer(Modifier.width(16.dp)); Column { Text(title, style = MaterialTheme.typography.titleMedium); Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray) } } } }
data class LogicalQuestion(val question: String, val options: List<String>, val correctAnswer: Int)
@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogicalTestScreen(navController: NavController) { val questions = remember { listOf(LogicalQuestion("Pola: 2, 4, 8, 16, ?", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Paus di air. Maka...", listOf("Paus ikan", "Paus bukan ikan", "Tak dapat disimpulkan"), 2)).shuffled() }; var currentQuestionIndex by remember { mutableIntStateOf(0) }; var score by remember { mutableIntStateOf(0) }; var isFinished by remember { mutableStateOf(false) }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Logika") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { padding -> Column(modifier = Modifier.padding(padding).padding(16.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (!isFinished) { val q = questions[currentQuestionIndex]; Text("Pertanyaan ${currentQuestionIndex + 1} / ${questions.size}"); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp)) }; Spacer(Modifier.height(24.dp)); q.options.forEachIndexed { index, option -> Button(onClick = { if (index == q.correctAnswer) score++; if (currentQuestionIndex < questions.size - 1) currentQuestionIndex++ else isFinished = true }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(option) } } } else { Text("Tes Selesai! Skor: $score/${questions.size}", style = MaterialTheme.typography.headlineMedium); Button(onClick = { navController.popBackStack() }) { Text("Kembali") } } } } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogicalTestScreen(navController: NavController) { val questions = remember { listOf(LogicalQuestion("Pola: 2, 4, 8, 16, ?", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Paus di air. Maka...", listOf("Paus ikan", "Paus bukan ikan", "Tak dapat disimpulkan"), 2)).shuffled() }; var currentQuestionIndex by remember { mutableIntStateOf(0) }; var score by remember { mutableIntStateOf(0) }; var isFinished by remember { mutableStateOf(false) }; val auth = Firebase.auth; Scaffold(topBar = { TopAppBar(title = { Text("Tes Logika") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { padding -> Column(modifier = Modifier.padding(padding).padding(16.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (!isFinished) { val q = questions[currentQuestionIndex]; Text("Pertanyaan ${currentQuestionIndex + 1} / ${questions.size}"); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp)) }; Spacer(Modifier.height(24.dp)); q.options.forEachIndexed { index, option -> Button(onClick = { if (index == q.correctAnswer) score++; if (currentQuestionIndex < questions.size - 1) currentQuestionIndex++ else { isFinished = true; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "logical", score) } }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(option) } } } else { Text("Tes Selesai! Skor: $score/${questions.size}", style = MaterialTheme.typography.headlineMedium); Button(onClick = { navController.popBackStack() }) { Text("Kembali") } } } } }
data class MemoryCard(val id: Int, val icon: ImageVector, var isFaceUp: Boolean = false, var isMatched: Boolean = false)
enum class MemoryGameState { READY, PLAYING, FINISHED }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryTestScreen(navController: NavController) { val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.ThumbUp, Icons.Default.Spa, Icons.Default.Cloud, Icons.Default.Anchor); var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> MemoryCard(i, icon) }.shuffled()) }; var selectedCards by remember { mutableStateOf(listOf<MemoryCard>()) }; var moves by remember { mutableIntStateOf(0) }; var gameState by remember { mutableStateOf(MemoryGameState.READY) }; LaunchedEffect(selectedCards) { if (selectedCards.size == 2) { val (c1, c2) = selectedCards; if (c1.icon == c2.icon) { cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isMatched = true) else it } } else { delay(1000); cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isFaceUp = false) else it } }; selectedCards = emptyList() } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Memori") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Column(Modifier.padding(p).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (gameState == MemoryGameState.READY) { Button(onClick = { gameState = MemoryGameState.PLAYING }) { Text("Mulai") } } else { Text("Gerakan: $moves"); LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { card -> MemoryCardView(card) { if (!card.isFaceUp && !card.isMatched && selectedCards.size < 2) { cards = cards.map { if (it.id == card.id) it.copy(isFaceUp = true) else it }; selectedCards = selectedCards + card; if (selectedCards.size == 1) moves++ } } } }; if (cards.all { it.isMatched }) { Text("Selesai! Skor: $moves"); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0; gameState=MemoryGameState.PLAYING }) { Text("Main Lagi") } } } } } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryTestScreen(navController: NavController) { val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.ThumbUp, Icons.Default.Spa, Icons.Default.Cloud, Icons.Default.Anchor); var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> MemoryCard(i, icon) }.shuffled()) }; var selectedCards by remember { mutableStateOf(listOf<MemoryCard>()) }; var moves by remember { mutableIntStateOf(0) }; var gameState by remember { mutableStateOf(MemoryGameState.READY) }; val auth = Firebase.auth; LaunchedEffect(selectedCards) { if (selectedCards.size == 2) { val (c1, c2) = selectedCards; if (c1.icon == c2.icon) { cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isMatched = true) else it } } else { delay(1000); cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isFaceUp = false) else it } }; selectedCards = emptyList() } }; LaunchedEffect(cards) { if (gameState == MemoryGameState.PLAYING && cards.all { it.isMatched }) { gameState = MemoryGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "memory", moves) } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Memori") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Column(Modifier.padding(p).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (gameState == MemoryGameState.READY) { Button(onClick = { gameState = MemoryGameState.PLAYING }) { Text("Mulai") } } else { Text("Gerakan: $moves"); LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { card -> MemoryCardView(card) { if (!card.isFaceUp && !card.isMatched && selectedCards.size < 2) { cards = cards.map { if (it.id == card.id) it.copy(isFaceUp = true) else it }; selectedCards = selectedCards + card; if (selectedCards.size == 1) moves++ } } } }; if (gameState == MemoryGameState.FINISHED) { Text("Selesai! Skor: $moves"); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0; gameState=MemoryGameState.PLAYING }) { Text("Main Lagi") } } } } } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryCardView(card: MemoryCard, onClick: () -> Unit) { Card(onClick = onClick, modifier = Modifier.padding(4.dp).aspectRatio(1f)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null) } } }
enum class FocusGameState { READY, PLAYING, FINISHED }
data class FocusItem(val icon: ImageVector, val color: Color, val rotation: Float, val isDistractor: Boolean)
@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; LaunchedEffect(gameState) { if (gameState == FocusGameState.PLAYING) { timeLeft = selectedDuration; while (timeLeft > 0) { delay(1000); timeLeft-- }; if (timeLeft == 0) gameState = FocusGameState.FINISHED } else if (gameState == FocusGameState.FINISHED) { if (score > highScore) { highScore = score } } }; fun newLevel() { gridItems = generateFocusGrid(normalColor) }; fun restartGame() { score = 0; gameState = FocusGameState.READY; newLevel() }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { when (gameState) { FocusGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Fokus"); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score"); Text("Waktu: $timeLeft") }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, modifier = Modifier.clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!"); Text("Skor Akhir: $score"); Button(onClick = { restartGame() }) { Text("Coba Lagi") } } } } } } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; val auth = Firebase.auth; LaunchedEffect(gameState) { if (gameState == FocusGameState.PLAYING) { timeLeft = selectedDuration; while (timeLeft > 0) { delay(1000); timeLeft-- }; if (timeLeft == 0) { gameState = FocusGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "focus", score) } } else if (gameState == FocusGameState.FINISHED) { if (score > highScore) { highScore = score } } }; fun newLevel() { gridItems = generateFocusGrid(normalColor) }; fun restartGame() { score = 0; gameState = FocusGameState.READY; newLevel() }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { when (gameState) { FocusGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Fokus"); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score"); Spacer(Modifier.width(16.dp)); Text("Waktu: $timeLeft") }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, modifier = Modifier.size(48.dp).rotate(item.rotation).clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }, tint = item.color) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!"); Text("Skor Akhir: $score"); Button(onClick = { restartGame() }) { Text("Coba Lagi") } } } } } } }
private fun generateFocusGrid(normalColor: Color): List<FocusItem> { val gridSize = 25; val normalIcon = Icons.Default.Circle; val distractorIndex = Random.nextInt(gridSize); val distractorType = Random.nextInt(3); val distractor: FocusItem; val items = MutableList(gridSize) { FocusItem(normalIcon, normalColor, 0f, false) }; when (distractorType) { 0 -> { distractor = FocusItem(Icons.Default.Star, normalColor, 0f, true) }; 1 -> { distractor = FocusItem(normalIcon, Color.Red, 0f, true) }; else -> { distractor = FocusItem(Icons.Default.Navigation, normalColor, 90f, true); items.replaceAll { it.copy(icon = Icons.Default.Navigation) } } }; items[distractorIndex] = distractor; return items }
enum class ReactionGameState { READY, WAITING, ACTION, FINISHED }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReactionSpeedTestScreen(navController: NavController) { var state by remember { mutableStateOf(ReactionGameState.READY) }; var startTime by remember { mutableLongStateOf(0L) }; var reactionTime by remember { mutableLongStateOf(0L) }; var bestTime by remember { mutableStateOf<Long?>(null) }; val backgroundColor by animateColorAsState(targetValue = when (state) { ReactionGameState.WAITING -> Color.Red; ReactionGameState.ACTION -> Color.Green; else -> MaterialTheme.colorScheme.surface }, label=""); val onScreenClick = { when (state) { ReactionGameState.WAITING -> { reactionTime = -1; state = ReactionGameState.FINISHED }; ReactionGameState.ACTION -> { val newTime = System.currentTimeMillis() - startTime; reactionTime = newTime; if (bestTime == null || newTime < bestTime!!) { bestTime = newTime }; state = ReactionGameState.FINISHED }; else -> {} } }; LaunchedEffect(state) { if (state == ReactionGameState.WAITING) { delay(Random.nextLong(1500, 5500)); if (state == ReactionGameState.WAITING) { startTime = System.currentTimeMillis(); state = ReactionGameState.ACTION } } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Kecepatan Reaksi") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding).background(backgroundColor).clickable(enabled = state == ReactionGameState.WAITING || state == ReactionGameState.ACTION, onClick = onScreenClick), contentAlignment = Alignment.Center) { when (state) { ReactionGameState.READY -> { Column { Text("Tes Kecepatan Reaksi"); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...") }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!") }; ReactionGameState.FINISHED -> { Column { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReactionSpeedTestScreen(navController: NavController) { var state by remember { mutableStateOf(ReactionGameState.READY) }; var startTime by remember { mutableLongStateOf(0L) }; var reactionTime by remember { mutableLongStateOf(0L) }; var bestTime by remember { mutableStateOf<Long?>(null) }; val auth = Firebase.auth; val backgroundColor by animateColorAsState(targetValue = when (state) { ReactionGameState.WAITING -> Color.Red; ReactionGameState.ACTION -> Color.Green; else -> MaterialTheme.colorScheme.surface }, label=""); val onScreenClick: () -> Unit = { when (state) { ReactionGameState.WAITING -> { reactionTime = -1; state = ReactionGameState.FINISHED }; ReactionGameState.ACTION -> { val newTime = System.currentTimeMillis() - startTime; reactionTime = newTime; if (bestTime == null || newTime < bestTime!!) { bestTime = newTime }; state = ReactionGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "reaction", newTime.toInt()) }; else -> {} } }; LaunchedEffect(state) { if (state == ReactionGameState.WAITING) { delay(Random.nextLong(1500, 5500)); if (state == ReactionGameState.WAITING) { startTime = System.currentTimeMillis(); state = ReactionGameState.ACTION } } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Kecepatan Reaksi") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding).background(backgroundColor).clickable(enabled = state == ReactionGameState.WAITING || state == ReactionGameState.ACTION, onClick = onScreenClick), contentAlignment = Alignment.Center) { when (state) { ReactionGameState.READY -> { Column { Text("Tes Kecepatan Reaksi"); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...") }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!") }; ReactionGameState.FINISHED -> { Column { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } }
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View File

@ -1,5 +1,5 @@
[versions]
agp = "8.13.1"
agp = "8.13.2"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"