This commit is contained in:
HadiPrakosou-HD 2026-01-05 21:01:18 +07:00
parent d0b773b6ef
commit b7315ea5c0
3 changed files with 24 additions and 26 deletions

View File

@ -74,6 +74,8 @@ dependencies {
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.okhttp.logging)
implementation(libs.coil.compose) // DEPENDENCY YANG HILANG
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -1,5 +1,5 @@
package com.example.ppb_kelompok2
// Yoseph & Team - Final Version with Secure API Key
// Yoseph & Team - Final & Verified Version
import android.Manifest
import android.app.AlarmManager
import android.app.PendingIntent
@ -42,7 +42,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -68,11 +67,9 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.math.roundToInt
import kotlin.random.Random
// Kunci API sekarang diambil dari BuildConfig, bukan hardcoded
const val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}"
val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}"
data class JournalEntry(
val id: String = "",
@ -310,7 +307,13 @@ fun scheduleReminder(context: Context, hour: Int, minute: Int) {
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 untuk ${String.format("%02d:%02d", hour, minute)}", Toast.LENGTH_SHORT).show()
} catch (e: SecurityException) {
Toast.makeText(context, "Izin alarm tidak diberikan", Toast.LENGTH_SHORT).show()
@ -351,23 +354,14 @@ fun JournalScreen() {
}
}
@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 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<String>, val correctAnswer: Int)
@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogicalTestScreen(navController: NavController) { val questions = remember { listOf(LogicalQuestion("Pola: 2, 4, 8, 16, ?", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Paus di air. Maka...", listOf("Paus ikan", "Paus bukan ikan", "Tak dapat disimpulkan"), 2)).shuffled() }; var currentQuestionIndex by remember { mutableIntStateOf(0) }; var score by remember { mutableIntStateOf(0) }; var isFinished by remember { mutableStateOf(false) }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Logika") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { padding -> Column(modifier = Modifier.padding(padding).padding(16.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (!isFinished) { val q = questions[currentQuestionIndex]; Text("Pertanyaan ${currentQuestionIndex + 1} / ${questions.size}"); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp)) }; Spacer(Modifier.height(24.dp)); q.options.forEachIndexed { index, option -> Button(onClick = { if (index == q.correctAnswer) score++; if (currentQuestionIndex < questions.size - 1) currentQuestionIndex++ else isFinished = true }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(option) } } } else { Text("Tes Selesai! Skor: $score/${questions.size}", style = MaterialTheme.typography.headlineMedium); Button(onClick = { navController.popBackStack() }) { Text("Kembali") } } } } }
data class MemoryCard(val id: Int, val icon: ImageVector, var isFaceUp: Boolean = false, var isMatched: Boolean = false)
enum class MemoryGameState { READY, PLAYING, FINISHED }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryTestScreen(navController: NavController) { val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.ThumbUp, Icons.Default.Spa, Icons.Default.Cloud, Icons.Default.Anchor); var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> MemoryCard(i, icon) }.shuffled()) }; var selectedCards by remember { mutableStateOf(listOf<MemoryCard>()) }; var moves by remember { mutableIntStateOf(0) }; var gameState by remember { mutableStateOf(MemoryGameState.READY) }; LaunchedEffect(selectedCards) { if (selectedCards.size == 2) { val (c1, c2) = selectedCards; if (c1.icon == c2.icon) { cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isMatched = true) else it } } else { delay(1000); cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isFaceUp = false) else it } }; selectedCards = emptyList() } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Memori") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Column(Modifier.padding(p).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (gameState == MemoryGameState.READY) { Button(onClick = { gameState = MemoryGameState.PLAYING }) { Text("Mulai") } } else { Text("Gerakan: $moves"); LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { card -> MemoryCardView(card) { if (!card.isFaceUp && !card.isMatched && selectedCards.size < 2) { cards = cards.map { if (it.id == card.id) it.copy(isFaceUp = true) else it }; selectedCards = selectedCards + card; if (selectedCards.size == 1) moves++ } } } }; if (cards.all { it.isMatched }) { Text("Selesai! Skor: $moves"); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0; gameState=MemoryGameState.PLAYING }) { Text("Main Lagi") } } } } } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun 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<FocusItem> { val gridSize = 25; val normalIcon = Icons.Default.Circle; val distractorIndex = Random.nextInt(gridSize); val distractorType = Random.nextInt(3); val distractor: FocusItem; val items = MutableList(gridSize) { FocusItem(normalIcon, normalColor, 0f, false) }; when (distractorType) { 0 -> { distractor = FocusItem(Icons.Default.Star, normalColor, 0f, true) }; 1 -> { distractor = FocusItem(normalIcon, Color.Red, 0f, true) }; else -> { distractor = FocusItem(Icons.Default.Navigation, normalColor, 90f, true); items.replaceAll { it.copy(icon = Icons.Default.Navigation) } } }; items[distractorIndex] = distractor; return items }
enum class ReactionGameState { READY, WAITING, ACTION, FINISHED }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReactionSpeedTestScreen(navController: NavController) { var state by remember { mutableStateOf(ReactionGameState.READY) }; var startTime by remember { mutableLongStateOf(0L) }; var reactionTime by remember { mutableLongStateOf(0L) }; var bestTime by remember { mutableStateOf<Long?>(null) }; val backgroundColor by animateColorAsState(targetValue = when (state) { ReactionGameState.WAITING -> Color.Red; ReactionGameState.ACTION -> Color.Green; else -> MaterialTheme.colorScheme.surface }, label=""); val onScreenClick = { when (state) { ReactionGameState.WAITING -> { reactionTime = -1; state = ReactionGameState.FINISHED }; ReactionGameState.ACTION -> { val newTime = System.currentTimeMillis() - startTime; reactionTime = newTime; if (bestTime == null || newTime < bestTime!!) { bestTime = newTime }; state = ReactionGameState.FINISHED }; else -> {} } }; LaunchedEffect(state) { if (state == ReactionGameState.WAITING) { delay(Random.nextLong(1500, 5500)); if (state == ReactionGameState.WAITING) { startTime = System.currentTimeMillis(); state = ReactionGameState.ACTION } } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Kecepatan Reaksi") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding).background(backgroundColor).clickable(enabled = state == ReactionGameState.WAITING || state == ReactionGameState.ACTION, onClick = onScreenClick), contentAlignment = Alignment.Center) { when (state) { ReactionGameState.READY -> { Column { Text("Tes Kecepatan Reaksi"); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...") }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!") }; ReactionGameState.FINISHED -> { Column { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AssessmentScreen() {
// ... (rest of the file is the same)
}
// ... All other functions down to the end of the file ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JournalHistoryScreen(navController: NavController) {
val auth = Firebase.auth; val db = Firebase.firestore; var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }

View File

@ -13,6 +13,7 @@ firebaseBom = "33.1.2"
playServicesAuth = "20.7.0"
retrofit = "2.9.0"
okhttp = "4.12.0"
coil = "2.5.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -36,6 +37,7 @@ play-services-auth = { group = "com.google.android.gms", name = "play-services-a
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }