Compare commits

...

6 Commits

Author SHA1 Message Date
27ff57548f fix 2026-01-12 17:29:32 +07:00
d55ee98191 Merge remote-tracking branch 'origin/Submain' into Submain
# Conflicts:
#	app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt
2026-01-12 17:29:10 +07:00
8c765fc7e2 fix 2026-01-12 17:28:56 +07:00
85345c4052 fix 2026-01-12 17:25:22 +07:00
8253b4adeb fix 2026-01-12 17:15:00 +07:00
23ff80ec12 fix 2026-01-12 16:43:42 +07:00
3 changed files with 259 additions and 270 deletions

View File

@ -30,7 +30,6 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@ -41,9 +40,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
@ -76,13 +73,6 @@ import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.random.Random 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 --- // --- Constants ---
val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}"
@ -108,17 +98,8 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val customColorScheme = lightColorScheme( PPB_Kelompok2Theme {
primary = HeartRed, Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
onPrimary = Color.White,
primaryContainer = SoftPink,
secondary = MediumPink,
surface = Color.White,
background = PinkBackground
)
MaterialTheme(colorScheme = customColorScheme) {
Surface(modifier = Modifier.fillMaxSize(), color = PinkBackground) {
AppNavigationGraph() AppNavigationGraph()
} }
} }
@ -146,9 +127,6 @@ fun AppNavigationGraph() {
fun LoginScreen(navController: NavController) { fun LoginScreen(navController: NavController) {
val context = LocalContext.current val context = LocalContext.current
var isLoading by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(false) }
// --- OAUTH KOMENTAR (Jangan Dihapus) ---
/*
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken("532164852718-k3op2ns8b5v42k2e5qj8j1d7g2g1m3g9.apps.googleusercontent.com") .requestIdToken("532164852718-k3op2ns8b5v42k2e5qj8j1d7g2g1m3g9.apps.googleusercontent.com")
.requestEmail() .requestEmail()
@ -166,297 +144,301 @@ fun LoginScreen(navController: NavController) {
if (authTask.isSuccessful) { if (authTask.isSuccessful) {
navController.navigate("main") { popUpTo("login") { inclusive = true } } navController.navigate("main") { popUpTo("login") { inclusive = true } }
} else { } else {
Toast.makeText(context, "Gagal Masuk", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Login Gagal: ${authTask.exception?.message}", Toast.LENGTH_LONG).show()
} }
} }
} catch (e: ApiException) { } catch (e: ApiException) {
isLoading = false isLoading = false
Toast.makeText(context, "Google Error", Toast.LENGTH_SHORT).show() Log.w("LoginScreen", "Google sign in failed", e)
Toast.makeText(context, "Google Sign In Gagal. Cek SHA-1.", Toast.LENGTH_LONG).show()
} }
} else { } else {
isLoading = false isLoading = false
} }
} }
*/ Column(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
// UIUX Text("MindTrack AI", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold)
Box(modifier = Modifier.fillMaxSize().background(Brush.verticalGradient(listOf(Color.White, SoftPink)))) { Spacer(modifier = Modifier.height(8.dp))
Column( Text("Lacak kesehatan mental, latih otak, dan temukan ketenangan.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 32.dp))
modifier = Modifier.fillMaxSize().padding(24.dp), Spacer(modifier = Modifier.height(32.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))
//uiux
if (isLoading) { if (isLoading) {
CircularProgressIndicator(color = HeartRed) CircularProgressIndicator()
} else { } else {
Button( Button(onClick = { isLoading = true; launcher.launch(googleSignInClient.signInIntent) }, modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp)) {
onClick = { Icon(Icons.Default.AccountCircle, contentDescription = "Google Icon")
// Bypass Login Sementara (OAuth di-komentar) Spacer(modifier = Modifier.width(8.dp))
navController.navigate("main") { popUpTo("login") { inclusive = true } } Text("Masuk dengan Google")
},
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) { sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
object Journal : Screen("journal", "Jurnal", Icons.Default.EditNote) object Journal : Screen("journal", "Jurnal", Icons.Default.Book)
object Assessment : Screen("assessment", "Penilaian", Icons.Default.FactCheck) object Assessment : Screen("assessment", "Penilaian", Icons.Default.Checklist)
object CognitiveTest : Screen("cognitive_test", "Latihan", Icons.Default.AutoAwesome) object CognitiveTest : Screen("cognitive_test", "Tes Kognitif", Icons.Default.SportsEsports)
object Profile : Screen("profile", "Profil", Icons.Default.Favorite) object Profile : Screen("profile", "Profil", Icons.Default.Person)
} }
val bottomNavItems = listOf(Screen.Journal, Screen.Assessment, Screen.CognitiveTest, Screen.Profile)
@Composable @Composable
fun MainAppScreen() { fun MainAppScreen() {
val navController = rememberNavController() val navController = rememberNavController()
Scaffold( Scaffold(bottomBar = { AppBottomNavigation(navController = navController) }) { innerPadding ->
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)) { NavHost(navController = navController, startDestination = Screen.Journal.route, modifier = Modifier.padding(innerPadding)) {
composable(Screen.Journal.route) { JournalScreen() } composable(Screen.Journal.route) { JournalScreen() }
composable(Screen.Assessment.route) { AssessmentScreen() } composable(Screen.Assessment.route) { AssessmentScreen() }
composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) } composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) }
composable(Screen.Profile.route) { ProfileScreen(navController) } composable(Screen.Profile.route) { ProfileScreen(navController = navController) }
composable("memory_test") { MemoryTestScreen(navController) } composable("memory_test") { MemoryTestScreen(navController) }
composable("focus_test") { FocusTestScreen(navController) } composable("focus_test") { FocusTestScreen(navController) }
composable("reaction_test") { ReactionSpeedTestScreen(navController) } composable("reaction_test") { ReactionSpeedTestScreen(navController) }
composable("logical_test") { LogicalTestScreen(navController) } composable("logical_test") { LogicalTestScreen(navController) }
composable("journal_history") { JournalHistoryScreen(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
}
}
)
} }
} }
} }
// --- Logic ---
fun calculateDailyStreak(journals: List<JournalEntry>): Int { fun calculateDailyStreak(journals: List<JournalEntry>): Int {
if (journals.isEmpty()) return 0 if (journals.isEmpty()) return 0
val entryDates = journals.mapNotNull { it.timestamp?.toDate() } val entryDates = journals.map { it.timestamp?.toDate() ?: Date(0) }.map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }.distinct().sortedDescending()
.map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }
.distinct().sortedDescending()
var streak = 0 var streak = 0
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
var currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) val todayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
if (currentDate !in entryDates) { if (todayStr in entryDates) {
calendar.add(Calendar.DATE, -1)
currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
}
while (currentDate in entryDates) {
streak++ streak++
calendar.add(Calendar.DATE, -1) calendar.add(Calendar.DATE, -1)
currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) } 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
}
} }
return streak return streak
} }
fun getBadges(journals: List<JournalEntry>, streak: Int): List<Badge> { fun getBadges(journals: List<JournalEntry>, streak: Int): List<Badge> {
val total = journals.size val totalEntries = journals.size
val reflectionEntries = journals.count { it.type == "reflection" }
val highStressEntries = journals.count { it.mentalScore > 60 }
return listOf( return listOf(
Badge("Sayang Diri", "Mulai Jurnal", Icons.Default.Favorite, total >= 1), Badge("Jurnalis Pertama", "Menulis jurnal pertamamu", Icons.Default.Edit, totalEntries >= 1),
Badge("Hati Teguh", "Streak 7 Hari", Icons.Default.Whatshot, streak >= 7), Badge("Seminggu Penuh", "Streak jurnaling 7 hari", Icons.Default.CalendarToday, streak >= 7),
Badge("Pencatat Setia", "10 Jurnal", Icons.Default.AutoGraph, total >= 10) 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)
) )
} }
fun calculateMentalHealthScore(sentiment: String, score: Float, indicators: Map<String, Float>): 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<String, Float>().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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ProfileScreen(navController: NavController) { fun ProfileScreen(navController: NavController) {
val user = Firebase.auth.currentUser val auth = Firebase.auth
val user = auth.currentUser
val db = Firebase.firestore val db = Firebase.firestore
var journals by remember { mutableStateOf<List<JournalEntry>>(emptyList()) } var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
LaunchedEffect(Unit) { if (user != null) db.collection("journals").whereEqualTo("userId", user.uid).get().addOnSuccessListener { journals = it.toObjects(JournalEntry::class.java) } } var dailyStreak by remember { mutableIntStateOf(0) }
val streak = calculateDailyStreak(journals) var badges by remember { mutableStateOf<List<Badge>>(emptyList()) }
val badges = getBadges(journals, streak) LaunchedEffect(user) {
if (user != null) {
LazyColumn(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { db.collection("journals").whereEqualTo("userId", user.uid).orderBy("timestamp").get().addOnSuccessListener { result ->
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) } } } } val journals = result.toObjects(JournalEntry::class.java)
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)) } } } 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) } } } }
item { item {
Text("Lencana", fontWeight = FontWeight.Bold, color = DeepRed) Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth())
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) } } } LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 100.dp), contentPadding = PaddingValues(top = 8.dp), modifier = Modifier.height(120.dp).fillMaxWidth()) {
items(badges) { BadgeItem(badge = it) }
} }
item { Button(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth().height(56.dp), colors = ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = HeartRed)) { Text("Riwayat Jurnal") } } }
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") } }
} }
} }
@Composable @Composable
fun CognitiveTestScreen(navController: NavController) { fun BadgeItem(badge: Badge) {
Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(8.dp).alpha(if (badge.isUnlocked) 1f else 0.4f)) {
Text("Latihan Otak", style = MaterialTheme.typography.headlineMedium, color = DeepRed, fontWeight = FontWeight.Bold) Icon(badge.icon, contentDescription = badge.title, modifier = Modifier.size(48.dp), tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray)
listOf("Memory Match" to "memory_test", "Fokus" to "focus_test", "Reaksi" to "reaction_test", "Logika" to "logical_test").forEach { (n, r) -> Spacer(modifier = Modifier.height(4.dp))
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) } } 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)
}
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) }
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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MemoryTestScreen(navController: NavController) { fun JournalScreen() {
val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.Face, Icons.Default.ThumbUp) 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")
var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> i to icon }.shuffled()) } Scaffold(topBar = { TopAppBar(title = { Text("Jurnal & Refleksi") }, actions = { IconButton(onClick = { timePickerDialog.show() }) { Icon(Icons.Default.Alarm, contentDescription = "Set Reminder") } }) }) { padding ->
var flipped by remember { mutableStateOf(setOf<Int>()) } Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
var matched by remember { mutableStateOf(setOf<Int>()) } TabRow(selectedTabIndex = selectedTab) { Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }, text = { Text("Jurnal Bebas") }); Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }, text = { Text("Refleksi") }) }
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() } } Spacer(modifier = Modifier.height(16.dp))
Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) { if (selectedTab == 0) {
IconButton({ navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) } 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) } }
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) } } } } 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<String, Float>(); 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<String>(); 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") } }
}
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @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(); 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 @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)) } } }
fun FocusTestScreen(navController: NavController) { @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) } }
var score by remember { mutableIntStateOf(0) }; var target by remember { mutableIntStateOf(Random.nextInt(25)) } @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) } } } }
Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) { data class LogicalQuestion(val question: String, val options: List<String>, val correctAnswer: Int)
IconButton({ navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) } @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") } } } } }
Text("Skor: $score", color = DeepRed, style = MaterialTheme.typography.headlineSmall) data class MemoryCard(val id: Int, val icon: ImageVector, var isFaceUp: Boolean = false, var isMatched: Boolean = false)
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) } } } } 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 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 }
@Composable data class FocusItem(val icon: ImageVector, val color: Color, val rotation: Float, val isDistractor: Boolean)
fun ReactionSpeedTestScreen(navController: NavController) { @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") } } } } } } }
var color by remember { mutableStateOf(Color.White) }; var startTime by remember { mutableLongStateOf(0L) }; var result by remember { mutableStateOf("") } 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 }
LaunchedEffect(Unit) { delay(Random.nextLong(2000, 5000)); color = HeartRed; startTime = System.currentTimeMillis() } enum class ReactionGameState { READY, WAITING, ACTION, FINISHED }
Box(Modifier.fillMaxSize().background(color).clickable { if(color == HeartRed && result.isEmpty()) result = "${System.currentTimeMillis() - startTime}ms" }, contentAlignment = Alignment.Center) { @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") } } } } } } }
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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun JournalHistoryScreen(navController: NavController) { fun JournalHistoryScreen(navController: NavController) {
val user = Firebase.auth.currentUser; var list by remember { mutableStateOf<List<JournalEntry>>(emptyList()) } val auth = Firebase.auth; val db = Firebase.firestore; var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
LaunchedEffect(Unit) { if (user != null) Firebase.firestore.collection("journals").whereEqualTo("userId", user.uid).get().addOnSuccessListener { list = it.toObjects(JournalEntry::class.java) } } 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<String, Float> ?: 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", color = DeepRed) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = PinkBackground), navigationIcon = { IconButton({navController.popBackStack()}) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) } }) }) { p -> Scaffold(topBar = { TopAppBar(title = { Text("Riwayat Jurnal") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) {
LazyColumn(Modifier.padding(p).background(PinkBackground).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { LazyColumn(modifier = Modifier.padding(it).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
item { if(list.isNotEmpty()) TrendGraph(list) } item { Text("Semua Entri Jurnal Anda", style = MaterialTheme.typography.headlineSmall) }
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) } } } 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) }) }
}
}
}
}
}
} }
} }
} }
//eee
@Composable @Composable
fun TrendGraph(journals: List<JournalEntry>) { fun TrendGraph(journals: List<JournalEntry>) {
val points = journals.takeLast(7).map { it.mentalScore }; if(points.size < 2) return if (journals.size < 2) {
Canvas(Modifier.fillMaxWidth().height(100.dp)) { Box(modifier = Modifier.fillMaxWidth().height(220.dp), contentAlignment = Alignment.Center) {
val step = size.width / (points.size - 1); val path = Path() Text("Grafik akan muncul setelah ada minimal 2 jurnal.", textAlign = TextAlign.Center)
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)) } 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))
}
}
}
} }
} }
class MyReminderReceiver : BroadcastReceiver() { override fun onReceive(c: Context, i: Intent) {} } class MyReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {}
}

View File

@ -2,10 +2,17 @@ package com.example.ppb_kelompok2.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
// Pink Muda, Merah, dan Putih Theme
val PinkMuda = Color(0xFFFFD1DC) // Pink sangat muda
val MerahUtama = Color(0xFFFF5252) // Merah cerah
val MerahTua = Color(0xFFD32F2F) // Merah gelap untuk teks/aksen
val PutihMurni = Color(0xFFFFFFFF) // Putih
val PinkAksen = Color(0xFFFFB6C1) // Pink untuk variasi
// Tetap pertahankan default jika diperlukan (opsional)
val Purple80 = Color(0xFFD0BCFF) val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC) val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8) val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4) val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71) val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260) val Pink40 = Color(0xFF7D5260)

View File

@ -9,35 +9,36 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = MerahUtama,
secondary = PurpleGrey80, secondary = PinkAksen,
tertiary = Pink80 tertiary = PinkMuda,
background = Color(0xFF1C1B1F),
surface = Color(0xFF1C1B1F),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.Black
) )
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = MerahUtama,
secondary = PurpleGrey40, secondary = MerahTua,
tertiary = Pink40 tertiary = PinkMuda,
background = PutihMurni,
/* Other default colors to override surface = PutihMurni,
background = Color(0xFFFFFBFE), onPrimary = PutihMurni,
surface = Color(0xFFFFFBFE), onSecondary = PutihMurni,
onPrimary = Color.White, onTertiary = Color.Black,
onSecondary = Color.White, surfaceVariant = PinkMuda
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
) )
@Composable @Composable
fun PPB_Kelompok2Theme( fun PPB_Kelompok2Theme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ dynamicColor: Boolean = false,
dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val colorScheme = when {
@ -45,7 +46,6 @@ fun PPB_Kelompok2Theme(
val context = LocalContext.current val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }
darkTheme -> DarkColorScheme darkTheme -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }