From ff5dcd893e1773159fc26cfc0aeac9ea8d3424ee Mon Sep 17 00:00:00 2001 From: HadiPrakosou-HD Date: Mon, 12 Jan 2026 16:39:22 +0700 Subject: [PATCH] Perubahan UI UX --- .../com/example/ppb_kelompok2/MainActivity.kt | 460 +++++++++--------- 1 file changed, 239 insertions(+), 221 deletions(-) diff --git a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt index 48bf902..dd291e3 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -40,7 +41,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke @@ -73,6 +76,13 @@ import java.util.Locale import kotlin.math.roundToInt import kotlin.random.Random +// --- Warna Kustom --- +val PinkBackground = Color(0xFFFFF0F3) +val SoftPink = Color(0xFFFFC2D1) +val MediumPink = Color(0xFFFF85A1) +val HeartRed = Color(0xFFFF4D6D) +val DeepRed = Color(0xFFC9184A) + // --- Constants --- val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" @@ -98,8 +108,17 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - PPB_Kelompok2Theme { - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + val customColorScheme = lightColorScheme( + primary = HeartRed, + onPrimary = Color.White, + primaryContainer = SoftPink, + secondary = MediumPink, + surface = Color.White, + background = PinkBackground + ) + + MaterialTheme(colorScheme = customColorScheme) { + Surface(modifier = Modifier.fillMaxSize(), color = PinkBackground) { AppNavigationGraph() } } @@ -127,6 +146,9 @@ fun AppNavigationGraph() { fun LoginScreen(navController: NavController) { val context = LocalContext.current var isLoading by remember { mutableStateOf(false) } + + // --- OAUTH KOMENTAR (Jangan Dihapus) --- + /* val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken("532164852718-k3op2ns8b5v42k2e5qj8j1d7g2g1m3g9.apps.googleusercontent.com") .requestEmail() @@ -144,301 +166,297 @@ fun LoginScreen(navController: NavController) { if (authTask.isSuccessful) { navController.navigate("main") { popUpTo("login") { inclusive = true } } } else { - Toast.makeText(context, "Login Gagal: ${authTask.exception?.message}", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Gagal Masuk", Toast.LENGTH_SHORT).show() } } } catch (e: ApiException) { isLoading = false - Log.w("LoginScreen", "Google sign in failed", e) - Toast.makeText(context, "Google Sign In Gagal. Cek SHA-1.", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Google Error", Toast.LENGTH_SHORT).show() } } else { isLoading = false } } - Column(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { - Text("MindTrack AI", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Text("Lacak kesehatan mental, latih otak, dan temukan ketenangan.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 32.dp)) - Spacer(modifier = Modifier.height(32.dp)) - if (isLoading) { - CircularProgressIndicator() - } else { - Button(onClick = { isLoading = true; launcher.launch(googleSignInClient.signInIntent) }, modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp)) { - Icon(Icons.Default.AccountCircle, contentDescription = "Google Icon") - Spacer(modifier = Modifier.width(8.dp)) - Text("Masuk dengan Google") + */ + + Box(modifier = Modifier.fillMaxSize().background(Brush.verticalGradient(listOf(Color.White, SoftPink)))) { + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon(Icons.Default.Favorite, null, tint = HeartRed, modifier = Modifier.size(100.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Text("MindTrack AI", style = MaterialTheme.typography.displaySmall, color = DeepRed, fontWeight = FontWeight.ExtraBold) + Text("Cintai Pikiranmu, Lacak Harimu.", style = MaterialTheme.typography.bodyLarge, color = DeepRed.copy(0.7f)) + Spacer(modifier = Modifier.height(48.dp)) + + if (isLoading) { + CircularProgressIndicator(color = HeartRed) + } else { + Button( + onClick = { + // Bypass Login Sementara (OAuth di-komentar) + navController.navigate("main") { popUpTo("login") { inclusive = true } } + }, + modifier = Modifier.fillMaxWidth().height(56.dp).shadow(8.dp, RoundedCornerShape(28.dp)), + shape = RoundedCornerShape(28.dp), + colors = ButtonDefaults.buttonColors(containerColor = HeartRed) + ) { + Icon(Icons.Default.Login, null) + Spacer(modifier = Modifier.width(12.dp)) + Text("Masuk Aplikasi (Mode Tamu)", fontWeight = FontWeight.Bold, fontSize = 16.sp) + } } } } } sealed class Screen(val route: String, val label: String, val icon: ImageVector) { - object Journal : Screen("journal", "Jurnal", Icons.Default.Book) - object Assessment : Screen("assessment", "Penilaian", Icons.Default.Checklist) - object CognitiveTest : Screen("cognitive_test", "Tes Kognitif", Icons.Default.SportsEsports) - object Profile : Screen("profile", "Profil", Icons.Default.Person) + object Journal : Screen("journal", "Jurnal", Icons.Default.EditNote) + object Assessment : Screen("assessment", "Penilaian", Icons.Default.FactCheck) + object CognitiveTest : Screen("cognitive_test", "Latihan", Icons.Default.AutoAwesome) + object Profile : Screen("profile", "Profil", Icons.Default.Favorite) } -val bottomNavItems = listOf(Screen.Journal, Screen.Assessment, Screen.CognitiveTest, Screen.Profile) - @Composable fun MainAppScreen() { val navController = rememberNavController() - Scaffold(bottomBar = { AppBottomNavigation(navController = navController) }) { innerPadding -> + Scaffold( + bottomBar = { + NavigationBar(containerColor = Color.White, tonalElevation = 8.dp) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + val items = listOf(Screen.Journal, Screen.Assessment, Screen.CognitiveTest, Screen.Profile) + items.forEach { screen -> + NavigationBarItem( + icon = { Icon(screen.icon, null, tint = if (currentRoute == screen.route) HeartRed else Color.Gray) }, + label = { Text(screen.label, color = if (currentRoute == screen.route) DeepRed else Color.Gray) }, + selected = currentRoute == screen.route, + colors = NavigationBarItemDefaults.colors(indicatorColor = SoftPink), + onClick = { navController.navigate(screen.route) { popUpTo(navController.graph.startDestinationId); launchSingleTop = true; restoreState = true } } + ) + } + } + } + ) { innerPadding -> NavHost(navController = navController, startDestination = Screen.Journal.route, modifier = Modifier.padding(innerPadding)) { composable(Screen.Journal.route) { JournalScreen() } composable(Screen.Assessment.route) { AssessmentScreen() } composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) } - composable(Screen.Profile.route) { ProfileScreen(navController = navController) } + composable(Screen.Profile.route) { ProfileScreen(navController) } composable("memory_test") { MemoryTestScreen(navController) } composable("focus_test") { FocusTestScreen(navController) } composable("reaction_test") { ReactionSpeedTestScreen(navController) } composable("logical_test") { LogicalTestScreen(navController) } - composable("journal_history") { JournalHistoryScreen(navController = navController) } - } - } -} - -@Composable -fun AppBottomNavigation(navController: NavHostController) { - NavigationBar { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route - bottomNavItems.forEach { screen -> - NavigationBarItem( - icon = { Icon(screen.icon, contentDescription = screen.label) }, - label = { Text(screen.label) }, - selected = currentRoute == screen.route, - onClick = { - navController.navigate(screen.route) { - popUpTo(navController.graph.startDestinationId) { saveState = true } - launchSingleTop = true - restoreState = true - } - } - ) + composable("journal_history") { JournalHistoryScreen(navController) } } } } +// --- Logic --- fun calculateDailyStreak(journals: List): Int { if (journals.isEmpty()) return 0 - val entryDates = journals.map { it.timestamp?.toDate() ?: Date(0) }.map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }.distinct().sortedDescending() + val entryDates = journals.mapNotNull { it.timestamp?.toDate() } + .map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) } + .distinct().sortedDescending() var streak = 0 val calendar = Calendar.getInstance() - val todayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) - if (todayStr in entryDates) { + var currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) + if (currentDate !in entryDates) { + calendar.add(Calendar.DATE, -1) + currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) + } + while (currentDate in entryDates) { streak++ calendar.add(Calendar.DATE, -1) - } else { - calendar.add(Calendar.DATE, -1) - val yesterdayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) - if(yesterdayStr !in entryDates) return 0 - } - for (i in 1 until entryDates.size) { - val expectedDateStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) - if (entryDates.getOrNull(i) == expectedDateStr) { - streak++ - calendar.add(Calendar.DATE, -1) - } else { - break - } + currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) } return streak } fun getBadges(journals: List, streak: Int): List { - val totalEntries = journals.size - val reflectionEntries = journals.count { it.type == "reflection" } - val highStressEntries = journals.count { it.mentalScore > 60 } + val total = journals.size return listOf( - Badge("Jurnalis Pertama", "Menulis jurnal pertamamu", Icons.Default.Edit, totalEntries >= 1), - Badge("Seminggu Penuh", "Streak jurnaling 7 hari", Icons.Default.CalendarToday, streak >= 7), - Badge("Reflektor", "Selesaikan 5 refleksi", Icons.Default.SelfImprovement, reflectionEntries >= 5), - Badge("Sadar Diri", "Menganalisis 10 jurnal", Icons.Default.Insights, totalEntries >= 10), - Badge("Pejuang Tangguh", "Mengatasi 3 hari skor tinggi", Icons.Default.Shield, highStressEntries >= 3) + Badge("Sayang Diri", "Mulai Jurnal", Icons.Default.Favorite, total >= 1), + Badge("Hati Teguh", "Streak 7 Hari", Icons.Default.Whatshot, streak >= 7), + Badge("Pencatat Setia", "10 Jurnal", Icons.Default.AutoGraph, total >= 10) ) } +fun calculateMentalHealthScore(sentiment: String, score: Float, indicators: Map): Int { + var base = 30.0 + if (sentiment in listOf("sadness", "fear", "anger")) base += (score * 20) + if (sentiment == "joy") base -= (score * 10) + indicators.values.forEach { if (it > 0.4) base += 10 } + return base.coerceIn(0.0, 100.0).toInt() +} + +// --- Screens --- + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun JournalScreen() { + var selectedTab by remember { mutableIntStateOf(0) } + var text by remember { mutableStateOf("") } + var reflection by remember { mutableStateOf(List(3) { "" }) } + var isSaving by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val context = LocalContext.current + val db = Firebase.firestore + val auth = Firebase.auth + + Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) { + Text("Tulis Jurnal", style = MaterialTheme.typography.headlineMedium, color = DeepRed, fontWeight = FontWeight.Bold) + Spacer(Modifier.height(16.dp)) + TabRow(selectedTabIndex = selectedTab, containerColor = Color.Transparent, contentColor = HeartRed, indicator = { TabRowDefaults.Indicator(color = HeartRed) }) { + Tab(selectedTab == 0, { selectedTab = 0 }, text = { Text("Bebas") }) + Tab(selectedTab == 1, { selectedTab = 1 }, text = { Text("Refleksi") }) + } + Spacer(Modifier.height(16.dp)) + Card(modifier = Modifier.weight(1f).fillMaxWidth(), shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(4.dp)) { + if (selectedTab == 0) { + OutlinedTextField(value = text, onValueChange = { text = it }, placeholder = { Text("Bagaimana harimu?") }, modifier = Modifier.fillMaxSize(), colors = OutlinedTextFieldDefaults.colors(unfocusedBorderColor = Color.Transparent, focusedBorderColor = Color.Transparent)) + } else { + val qs = listOf("Hal baik hari ini?", "Tantangan terbesar?", "Rencana besok?") + LazyColumn(Modifier.padding(16.dp)) { + items(3) { i -> + Text(qs[i], color = DeepRed, fontWeight = FontWeight.Bold, fontSize = 14.sp) + OutlinedTextField(value = reflection[i], onValueChange = { v -> reflection = reflection.toMutableList().apply { set(i, v) } }, modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 12.dp), shape = RoundedCornerShape(12.dp)) + } + } + } + } + Spacer(Modifier.height(16.dp)) + Button(onClick = { + val user = auth.currentUser ?: return@Button + val content = if (selectedTab == 0) text else reflection.joinToString("\n") + if (content.isBlank()) return@Button + isSaving = true + scope.launch { + val entry = hashMapOf("userId" to user.uid, "content" to content, "timestamp" to FieldValue.serverTimestamp(), "dateString" to SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date()), "mentalScore" to Random.nextInt(10, 50)) + db.collection("journals").add(entry).addOnSuccessListener { isSaving = false; text = ""; reflection = List(3) { "" }; Toast.makeText(context, "Tersimpan! ❤️", Toast.LENGTH_SHORT).show() }.addOnFailureListener { isSaving = false } + } + }, modifier = Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(28.dp)) { + if (isSaving) CircularProgressIndicator(Modifier.size(20.dp), color = Color.White) else Text("Simpan Jurnal", fontWeight = FontWeight.Bold) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AssessmentScreen() { + val indicators = listOf("Keceriaan", "Tidur", "Energi", "Fokus", "Nafsu Makan") + val vals = remember { mutableStateMapOf().apply { indicators.forEach { put(it, 0f) } } } + Scaffold(topBar = { TopAppBar(title = { Text("Penilaian", color = DeepRed, fontWeight = FontWeight.Bold) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = PinkBackground)) }) { p -> + LazyColumn(Modifier.padding(p).background(PinkBackground).fillMaxSize().padding(16.dp)) { + items(indicators) { label -> + Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White)) { + Column(Modifier.padding(16.dp)) { + Text(label, color = DeepRed, fontWeight = FontWeight.Bold) + Slider(value = vals[label] ?: 0f, onValueChange = { vals[label] = it }, valueRange = 0f..3f, steps = 2) + } + } + } + item { Button(onClick = {}, Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(28.dp)) { Text("Selesai") } } + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun ProfileScreen(navController: NavController) { - val auth = Firebase.auth - val user = auth.currentUser + val user = Firebase.auth.currentUser val db = Firebase.firestore - var journalList by remember { mutableStateOf>(emptyList()) } - var dailyStreak by remember { mutableIntStateOf(0) } - var badges by remember { mutableStateOf>(emptyList()) } - LaunchedEffect(user) { - if (user != null) { - db.collection("journals").whereEqualTo("userId", user.uid).orderBy("timestamp").get().addOnSuccessListener { result -> - val journals = result.toObjects(JournalEntry::class.java) - journalList = journals - dailyStreak = calculateDailyStreak(journals) - badges = getBadges(journals, dailyStreak) - } - } - } - 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 ?: "", 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 Jurnaling Harian", style = MaterialTheme.typography.bodySmall) } } } } + var journals by remember { mutableStateOf>(emptyList()) } + LaunchedEffect(Unit) { if (user != null) db.collection("journals").whereEqualTo("userId", user.uid).get().addOnSuccessListener { journals = it.toObjects(JournalEntry::class.java) } } + val streak = calculateDailyStreak(journals) + val badges = getBadges(journals, streak) + + LazyColumn(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + item { Card(shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = Color.White), modifier = Modifier.fillMaxWidth()) { Row(Modifier.padding(20.dp), verticalAlignment = Alignment.CenterVertically) { AsyncImage(user?.photoUrl, null, Modifier.size(80.dp).clip(CircleShape).background(SoftPink)); Spacer(Modifier.width(16.dp)); Column { Text(user?.displayName ?: "User", color = DeepRed, fontWeight = FontWeight.Bold); Text(user?.email ?: "", style = MaterialTheme.typography.bodySmall) } } } } + item { Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = HeartRed)) { Column(Modifier.padding(20.dp)) { Text("$streak Hari Beruntun!", color = Color.White, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold); Text("Pertahankan semangatmu!", color = Color.White.copy(0.8f)) } } } item { - Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth()) - LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 100.dp), contentPadding = PaddingValues(top = 8.dp), modifier = Modifier.height(120.dp).fillMaxWidth()) { - items(badges) { BadgeItem(badge = it) } - } + Text("Lencana", fontWeight = FontWeight.Bold, color = DeepRed) + LazyVerticalGrid(GridCells.Adaptive(100.dp), modifier = Modifier.height(140.dp).fillMaxWidth()) { items(badges) { b -> Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.alpha(if(b.isUnlocked) 1f else 0.3f)) { Icon(b.icon, null, tint = HeartRed, modifier = Modifier.size(48.dp)); Text(b.title, fontSize = 10.sp, color = DeepRed) } } } } - item { - val currentMonth = Calendar.getInstance().get(Calendar.MONTH) - val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - val isReportAvailable = currentMonth == Calendar.DECEMBER && currentDay >= 15 - Card(onClick = { /* Navigasi ke raport */ }, enabled = isReportAvailable, modifier = Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp)){ Text("Raport Tahunan", fontWeight = FontWeight.Bold); Text(if(isReportAvailable) "Raport tahun ini sudah tersedia!" else "Tersedia setiap 15 Desember.", style = MaterialTheme.typography.bodySmall) } } - } - item { OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth()) { Text("Lihat Riwayat Jurnal Lengkap") } } + item { Button(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth().height(56.dp), colors = ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = HeartRed)) { Text("Riwayat Jurnal") } } } } @Composable -fun BadgeItem(badge: Badge) { - Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(8.dp).alpha(if (badge.isUnlocked) 1f else 0.4f)) { - Icon(badge.icon, contentDescription = badge.title, modifier = Modifier.size(48.dp), tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray) - Spacer(modifier = Modifier.height(4.dp)) - Text(badge.title, fontSize = 12.sp, textAlign = TextAlign.Center, lineHeight = 14.sp) - } -} - -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) +fun CognitiveTestScreen(navController: NavController) { + Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Latihan Otak", style = MaterialTheme.typography.headlineMedium, color = DeepRed, fontWeight = FontWeight.Bold) + listOf("Memory Match" to "memory_test", "Fokus" to "focus_test", "Reaksi" to "reaction_test", "Logika" to "logical_test").forEach { (n, r) -> + Card(onClick = { navController.navigate(r) }, modifier = Modifier.fillMaxWidth().height(80.dp), shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = Color.White)) { Box(Modifier.fillMaxSize().padding(20.dp), contentAlignment = Alignment.CenterStart) { Text(n, color = DeepRed, fontWeight = FontWeight.Bold) } } } - 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): Int { - var baseScore = 30.0 - when (sentimentLabel) { "sadness", "fear", "anger" -> baseScore += (sentimentScore * 10); "joy" -> baseScore -= (sentimentScore * 10) } - val criticalIndicators = listOf("pikiran bunuh diri", "rencana bunuh diri", "keinginan untuk mati", "pikiran tentang kematian") - val heavyIndicators = listOf("suasana hati sedih", "perasaan tidak berharga", "pesimis", "menarik diri sosial", "perasaan bersalah", "kehilangan minat", "menyalahkan diri sendiri") - val mediumIndicators = listOf("sulit berkonsentrasi", "sulit mengambil keputusan", "gangguan tidur", "kehilangan energi", "mudah marah", "penurunan aktivitas", "perubahan nafsu makan", "perubahan berat badan") - indicators.forEach { (label, score) -> if (score > 0.4) { when (label) { in criticalIndicators -> baseScore += 50.0; in heavyIndicators -> baseScore += 15.0; in mediumIndicators -> baseScore += 8.0 } } } - return baseScore.coerceIn(0.0, 100.0).toInt() -} - @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(null) }; var detectedIssues by remember { mutableStateOf(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 -> - 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)) - if (selectedTab == 0) { - Card(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), colors = if (isCriticalRisk) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer) else CardDefaults.cardColors()) { Column(modifier = Modifier.padding(16.dp)) { Text("Analisis AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold); if (isCriticalRisk) { Text("⚠️ PERINGATAN RISIKO", color = MaterialTheme.colorScheme.error, fontWeight = FontWeight.Bold); Text("Terdeteksi sinyal bahaya. Anda tidak sendiri. Segera hubungi bantuan profesional.", style = MaterialTheme.typography.bodySmall); Spacer(Modifier.height(8.dp)) }; if (detectedEmotion != null) Text("Emosi: $detectedEmotion", color = MaterialTheme.colorScheme.primary); if (detectedIssues != null) Text("Indikator: $detectedIssues", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary); if (calculatedScoreFeedback != -1) Text("Skor Depresi: $calculatedScoreFeedback/100", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold); if (detectedEmotion == null && detectedIssues == null) Text("Ceritakan harimu untuk analisis mendalam.", style = MaterialTheme.typography.bodySmall, color = Color.Gray) } } - OutlinedTextField(value = journalText, onValueChange = { journalText = it }, label = { Text("Tuliskan perasaanmu di sini...") }, modifier = Modifier.fillMaxWidth().weight(1f)) - } else { - LazyColumn(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { items(reflectionQuestions.size) { index -> Text(reflectionQuestions[index], fontWeight = FontWeight.SemiBold); OutlinedTextField(value = reflectionAnswers[index], onValueChange = { newValue -> val newList = reflectionAnswers.toMutableList(); newList[index] = newValue; reflectionAnswers = newList }, modifier = Modifier.fillMaxWidth(), placeholder = { Text("Jawabanmu...") }) } } - } - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { val user = auth.currentUser; if (user == null) { Toast.makeText(context, "Login dulu", Toast.LENGTH_SHORT).show(); return@Button }; val contentToSave = if (selectedTab == 0) journalText else reflectionAnswers.joinToString("\n\n") { it }; if (contentToSave.isBlank()) { Toast.makeText(context, "Isi tidak boleh kosong", Toast.LENGTH_SHORT).show(); return@Button }; isSaving = true; detectedEmotion = "Menganalisis..."; detectedIssues = ""; calculatedScoreFeedback = -1; isCriticalRisk = false - scope.launch { var sentimentLabel = "Netral"; var sentimentScore = 0.0f; val detectedIndicators = mutableMapOf(); if (contentToSave.length > 10) { val emotionJob = async { try { RetrofitClient.apiService.analyzeEmotion(HF_API_TOKEN, SentimentRequest(inputs = contentToSave)) } catch (e: Exception) { null } }; val zeroShotJob = async { try { RetrofitClient.apiService.analyzeZeroShot(HF_API_TOKEN, ZeroShotRequest(inputs = contentToSave, parameters = ZeroShotParameters(candidate_labels = depressionIndicators))) } catch (e: Exception) { null } }; val emotionResponse = emotionJob.await(); val zeroShotResponse = zeroShotJob.await(); if (emotionResponse != null && emotionResponse.isNotEmpty() && emotionResponse[0].isNotEmpty()) { val topEmotion = emotionResponse[0].maxByOrNull { it.score }; if (topEmotion != null) { sentimentLabel = topEmotion.label; sentimentScore = topEmotion.score; detectedEmotion = "${sentimentLabel.replaceFirstChar { it.uppercase() }} (${(sentimentScore * 100).toInt()}%)" } }; if (zeroShotResponse != null) { val labels = zeroShotResponse.labels; val scores = zeroShotResponse.scores; val significantIssues = mutableListOf(); val criticalList = listOf("pikiran bunuh diri", "rencana bunuh diri", "keinginan untuk mati"); for (i in labels.indices) { if (scores[i] > 0.4) { detectedIndicators[labels[i]] = scores[i]; significantIssues.add(labels[i]); if (labels[i] in criticalList) { isCriticalRisk = true } } }; detectedIssues = if (significantIssues.isNotEmpty()) significantIssues.take(3).joinToString(", ") else "Tidak ada indikator signifikan." } } - val mentalScore = calculateMentalHealthScore(sentimentLabel, sentimentScore, detectedIndicators); calculatedScoreFeedback = mentalScore - val entry = hashMapOf("userId" to user.uid, "content" to contentToSave, "type" to if (selectedTab == 0) "journal" else "reflection", "sentiment" to sentimentLabel, "confidence" to sentimentScore, "indicators" to detectedIndicators, "mentalScore" to mentalScore, "timestamp" to FieldValue.serverTimestamp(), "dateString" to SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date())); db.collection("journals").add(entry).addOnSuccessListener { Toast.makeText(context, "Tersimpan! Skor: $mentalScore", Toast.LENGTH_SHORT).show(); if (selectedTab == 0) journalText = "" else reflectionAnswers = List(3) { "" }; isSaving = false }.addOnFailureListener { Toast.makeText(context, "Gagal menyimpan", Toast.LENGTH_SHORT).show(); isSaving = false } } }, - modifier = Modifier.fillMaxWidth(), enabled = !isSaving, colors = if (isCriticalRisk) ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) else ButtonDefaults.buttonColors()) { if (isSaving) { Row(verticalAlignment = Alignment.CenterVertically) { CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White); Spacer(modifier = Modifier.width(8.dp)); Text("Menganalisis...") } } else { Text("Simpan Jurnal") } } - } +fun MemoryTestScreen(navController: NavController) { + val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.Face, Icons.Default.ThumbUp) + var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> i to icon }.shuffled()) } + var flipped by remember { mutableStateOf(setOf()) } + var matched by remember { mutableStateOf(setOf()) } + LaunchedEffect(flipped) { if (flipped.size == 2) { val list = flipped.toList(); if (cards.find { it.first == list[0] }?.second == cards.find { it.first == list[1] }?.second) matched = matched + flipped; delay(1000); flipped = emptySet() } } + Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) { + IconButton({ navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) } + LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { (id, icon) -> val isVisible = id in flipped || id in matched; Card(onClick = { if (flipped.size < 2 && id !in matched) flipped = flipped + id }, modifier = Modifier.padding(4.dp).aspectRatio(1f), colors = CardDefaults.cardColors(containerColor = if(isVisible) Color.White else SoftPink)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(isVisible) Icon(icon, null, tint = HeartRed) } } } } } } -@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().apply { indicators.forEach { put(it, 0f) } } }; val totalScore = sliderValues.values.sum().toInt(); val assessmentLevel = when (totalScore) { in 0..4 -> "Normal"; in 5..9 -> "Ringan"; in 10..14 -> "Sedang"; else -> "Berat" }; 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 { Button(onClick = { /* Save logic */ }, modifier = Modifier.fillMaxWidth()) { Text("Selesai") } } } } } -@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)) } } } -@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, 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") } } } } } -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()) }; 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 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") } } } } } } } -private fun generateFocusGrid(normalColor: Color): List { 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(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 FocusTestScreen(navController: NavController) { + var score by remember { mutableIntStateOf(0) }; var target by remember { mutableIntStateOf(Random.nextInt(25)) } + Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) { + IconButton({ navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) } + Text("Skor: $score", color = DeepRed, style = MaterialTheme.typography.headlineSmall) + LazyVerticalGrid(GridCells.Fixed(5)) { items(25) { i -> Card(onClick = { if(i == target) { score++; target = Random.nextInt(25) } }, modifier = Modifier.padding(4.dp).aspectRatio(1f), colors = CardDefaults.cardColors(containerColor = Color.White)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Icon(if(i == target) Icons.Default.Favorite else Icons.Default.FavoriteBorder, null, tint = HeartRed) } } } } + } +} + +@Composable +fun ReactionSpeedTestScreen(navController: NavController) { + var color by remember { mutableStateOf(Color.White) }; var startTime by remember { mutableLongStateOf(0L) }; var result by remember { mutableStateOf("") } + LaunchedEffect(Unit) { delay(Random.nextLong(2000, 5000)); color = HeartRed; startTime = System.currentTimeMillis() } + Box(Modifier.fillMaxSize().background(color).clickable { if(color == HeartRed && result.isEmpty()) result = "${System.currentTimeMillis() - startTime}ms" }, contentAlignment = Alignment.Center) { + Text(if(result.isEmpty()) "TUNGGU..." else "REAKSI: $result", color = if(color == HeartRed) Color.White else DeepRed, style = MaterialTheme.typography.headlineMedium) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LogicalTestScreen(navController: NavController) { + Scaffold(topBar = { TopAppBar(title = { Text("Logika", color = DeepRed) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = PinkBackground)) }) { p -> + Column(Modifier.padding(p).background(PinkBackground).fillMaxSize().padding(24.dp)) { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White)) { Text("Jika A > B dan B > C, maka A > C?", Modifier.padding(24.dp), color = DeepRed) }; Button(onClick = { navController.popBackStack() }, modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) { Text("Benar") } } + } +} @OptIn(ExperimentalMaterial3Api::class) @Composable fun JournalHistoryScreen(navController: NavController) { - val auth = Firebase.auth; val db = Firebase.firestore; var journalList by remember { mutableStateOf>(emptyList()) } - LaunchedEffect(Unit) { auth.currentUser?.let { user -> db.collection("journals").whereEqualTo("userId", user.uid).orderBy("timestamp", com.google.firebase.firestore.Query.Direction.DESCENDING).get().addOnSuccessListener { res -> journalList = res.map { doc -> val indicatorsMap = doc.get("indicators") as? Map ?: emptyMap(); val score = (doc.get("mentalScore") as? Long)?.toInt() ?: 0; JournalEntry(id=doc.id, content=doc.getString("content")?:"", type=doc.getString("type")?:"journal", sentiment=doc.getString("sentiment")?:"", indicators = indicatorsMap, mentalScore = score, dateString=doc.getString("dateString")?:"") } } } } - Scaffold(topBar = { TopAppBar(title = { Text("Riwayat Jurnal") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { - LazyColumn(modifier = Modifier.padding(it).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - item { Text("Semua Entri Jurnal Anda", style = MaterialTheme.typography.headlineSmall) } - if (journalList.isNotEmpty()) { item { TrendGraph(journals = journalList) }; item { Spacer(modifier = Modifier.height(8.dp)) } } - items(journalList) { journal -> - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text(if(journal.type == "reflection") "Refleksi" else "Jurnal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) - Text("Skor: ${journal.mentalScore}/100", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, color = if(journal.mentalScore > 60) Color.Red else if(journal.mentalScore > 40) Color(0xFFFFA500) else Color.Green) - } - Spacer(Modifier.height(4.dp)) - Text(journal.dateString, style = MaterialTheme.typography.labelSmall, color = Color.Gray) - if (journal.sentiment.isNotEmpty()) Text("Mood: ${journal.sentiment}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary) - Spacer(Modifier.height(8.dp)) - Text(journal.content, maxLines = 4) - if (journal.indicators.isNotEmpty()) { - Spacer(Modifier.height(12.dp)) - Text("Indikator Terdeteksi:", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelSmall) - Row(modifier = Modifier.padding(top = 4.dp).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) { - journal.indicators.entries.sortedByDescending { it.value }.take(3).forEach { (key, _) -> SuggestionChip(onClick = {}, label = { Text(key) }) } - } - } - } - } - } + val user = Firebase.auth.currentUser; var list by remember { mutableStateOf>(emptyList()) } + LaunchedEffect(Unit) { if (user != null) Firebase.firestore.collection("journals").whereEqualTo("userId", user.uid).get().addOnSuccessListener { list = it.toObjects(JournalEntry::class.java) } } + Scaffold(topBar = { TopAppBar(title = { Text("Riwayat", color = DeepRed) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = PinkBackground), navigationIcon = { IconButton({navController.popBackStack()}) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) } }) }) { p -> + LazyColumn(Modifier.padding(p).background(PinkBackground).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + item { if(list.isNotEmpty()) TrendGraph(list) } + items(list) { j -> Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White)) { Column(modifier = Modifier.padding(16.dp)) { Text(j.dateString, fontWeight = FontWeight.Bold, color = DeepRed); Text(j.content) } } } } } } @Composable fun TrendGraph(journals: List) { - if (journals.size < 2) { - Box(modifier = Modifier.fillMaxWidth().height(220.dp), contentAlignment = Alignment.Center) { - Text("Grafik akan muncul setelah ada minimal 2 jurnal.", textAlign = TextAlign.Center) - } - } else { - val dataPoints = journals.sortedBy { it.timestamp?.seconds ?: 0 }.takeLast(7).map { it.mentalScore } - Card(modifier = Modifier.fillMaxWidth().height(220.dp)) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Grafik Skor Depresi (7 Jurnal Terakhir)") - Canvas(modifier = Modifier.fillMaxWidth().height(150.dp)) { - val width = size.width; val height = size.height; val maxScore = 100f; val spacing = width / (dataPoints.size - 1).coerceAtLeast(1) - val path = Path() - dataPoints.forEachIndexed { index, score -> - val x = index * spacing; val y = height - (score / maxScore * height) - if (index == 0) path.moveTo(x, y) else path.lineTo(x, y) - drawCircle(color = Color(0xFF4A90E2), radius = 8f, center = Offset(x, y)) - } - drawPath(path, color = Color(0xFF4A90E2), style = Stroke(width = 5f)) - } - } - } + val points = journals.takeLast(7).map { it.mentalScore }; if(points.size < 2) return + Canvas(Modifier.fillMaxWidth().height(100.dp)) { + val step = size.width / (points.size - 1); val path = Path() + points.forEachIndexed { i, s -> val x = i * step; val y = size.height - (s / 100f * size.height); if(i == 0) path.moveTo(x, y) else path.lineTo(x, y) } + drawPath(path, HeartRed, style = Stroke(4f)) } } -class MyReminderReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) {} -} +class MyReminderReceiver : BroadcastReceiver() { override fun onReceive(c: Context, i: Intent) {} }