From 7fe8f9a0df41b7e8ae52348f042594c987f2ed1b Mon Sep 17 00:00:00 2001 From: RyanMaulana23 Date: Mon, 22 Dec 2025 17:30:31 +0700 Subject: [PATCH 01/19] Oauth, Model ML, Firebase --- .idea/.name | 1 + .idea/codeStyles/Project.xml | 123 ++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/studiobot.xml | 6 + app/build.gradle.kts | 8 + app/google-services.json | 29 + app/src/main/AndroidManifest.xml | 13 +- .../example/ppb_kelompok2/HuggingFaceApi.kt | 66 + .../com/example/ppb_kelompok2/MainActivity.kt | 1175 +++++++---------- .../example/ppb_kelompok2/ReminderReceiver.kt | 62 + .../drawable/ic_mindtrack_logo_foreground.xml | 16 + .../ic_launcher_mindtrack.xml | 5 + .../ic_launcher_mindtrack_round.xml | 5 + app/src/main/res/values/colors.xml | 3 + build.gradle.kts | 1 + gradle/libs.versions.toml | 14 +- 16 files changed, 849 insertions(+), 683 deletions(-) create mode 100644 .idea/.name create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/studiobot.xml create mode 100644 app/google-services.json create mode 100644 app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt create mode 100644 app/src/main/java/com/example/ppb_kelompok2/ReminderReceiver.kt create mode 100644 app/src/main/res/drawable/ic_mindtrack_logo_foreground.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack_round.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..76d0635 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +PPB_Kelompok2 \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml new file mode 100644 index 0000000..539e3b8 --- /dev/null +++ b/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3f21c8..09b33cc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.google.services) } android { @@ -52,6 +53,13 @@ dependencies { implementation(libs.androidx.compose.material3) implementation("androidx.compose.material:material-icons-extended") implementation("androidx.navigation:navigation-compose:2.9.6") + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.auth.ktx) + implementation(libs.firebase.firestore.ktx) + implementation(libs.play.services.auth) + implementation(libs.retrofit) + implementation(libs.retrofit.gson) + implementation(libs.okhttp.logging) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..cd34937 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "532164852718", + "project_id": "jurnal-psikologi", + "storage_bucket": "jurnal-psikologi.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:532164852718:android:efd8ddbb729d947eaeecff", + "android_client_info": { + "package_name": "com.example.ppb_kelompok2" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBnq_cjey_sz1KfJQ1mCJlvK61lWEQATis" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c56ca6..51c117c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,13 +2,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt b/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt new file mode 100644 index 0000000..07fb3b2 --- /dev/null +++ b/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt @@ -0,0 +1,66 @@ +package com.example.ppb_kelompok2 + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST + +// --- 1. Sentiment Analysis Models --- +data class SentimentRequest(val inputs: String) + +typealias SentimentResponse = List> + +data class EmotionScore( + val label: String, + val score: Float +) + +// --- 2. Zero-Shot Classification Models w --- +data class ZeroShotRequest( + val inputs: String, + val parameters: ZeroShotParameters +) + +data class ZeroShotParameters( + val candidate_labels: List, + val multi_label: Boolean = true // True = satu teks bisa masuk ke banyak kategori +) + +data class ZeroShotResponse( + val sequence: String, + val labels: List, + val scores: List +) + +// --- 3. Interface API Definition --- +interface HuggingFaceApiService { + + // Model 1: Emosi (Inggris) - Cepat & Ringan + @POST("models/j-hartmann/emotion-english-distilroberta-base") + suspend fun analyzeEmotion( + @Header("Authorization") authHeader: String, + @Body request: SentimentRequest + ): SentimentResponse + + // Model 2: Zero-Shot Classification (Multilingual) - Lebih Berat tapi Detail + // Menggunakan joeddav/xlm-roberta-large-xnli untuk support Bahasa Indonesia + @POST("models/joeddav/xlm-roberta-large-xnli") + suspend fun analyzeZeroShot( + @Header("Authorization") authHeader: String, + @Body request: ZeroShotRequest + ): ZeroShotResponse +} + +// --- 4. Singleton Instance --- +object RetrofitClient { + private const val BASE_URL = "https://api-inference.huggingface.co/" + + val apiService: HuggingFaceApiService by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(HuggingFaceApiService::class.java) + } +} 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 d4c5490..588cdee 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -1,11 +1,25 @@ package com.example.ppb_kelompok2 -// Yoseph +// Yoseph & Team +import android.Manifest +import android.app.AlarmManager +import android.app.PendingIntent +import android.app.TimePickerDialog +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Paint +import android.os.Build import android.os.Bundle +import android.util.Log +import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -14,6 +28,8 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -21,20 +37,61 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.* import com.example.ppb_kelompok2.ui.theme.PPB_Kelompok2Theme +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.ktx.auth +import com.google.firebase.firestore.FieldValue +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase +import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale import kotlin.math.roundToInt import kotlin.random.Random +// --- Constants --- +// PENTING: Ganti token ini dengan User Access Token dari Hugging Face Anda! +const val HF_API_TOKEN = "Bearer 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" + +// --- Data Models --- +data class JournalEntry( + val id: String = "", + val userId: String = "", + val content: String = "", + val type: String = "journal", + val sentiment: String = "", + val confidence: Float = 0f, + val indicators: Map = emptyMap(), + val mentalScore: Int = 0, // Skor 0-100 (0 = Sehat, 100 = Sangat Tertekan) + val timestamp: com.google.firebase.Timestamp? = null, + val dateString: String = "" +) + // --- Main Activity --- class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -47,6 +104,12 @@ class MainActivity : ComponentActivity() { } } } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 101) + } + } } } @@ -54,9 +117,14 @@ class MainActivity : ComponentActivity() { @Composable fun AppNavigationGraph() { val navController = rememberNavController() - NavHost(navController = navController, startDestination = "login") { + val auth = Firebase.auth + val currentUser = auth.currentUser + + val startDestination = if (currentUser != null) "main" else "login" + + NavHost(navController = navController, startDestination = startDestination) { composable("login") { LoginScreen(navController = navController) } - composable("main") { MainAppScreen() } + composable("main") { MainAppScreen(navController = navController) } } } @@ -64,43 +132,82 @@ fun AppNavigationGraph() { // --- Login Screen --- @Composable fun LoginScreen(navController: NavController) { + val context = LocalContext.current + var isLoading by remember { mutableStateOf(false) } + + // Konfigurasi Google Sign In + // PENTING: "default_web_client_id" biasanya otomatis dari google-services.json + // Jika masih gagal, ganti getString(R.string.default_web_client_id) dengan STRING MANUAL Client ID dari Firebase Console + val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken("532164852718-363oc5qe1vvolcaof4ocpbepu4o59438.apps.googleusercontent.com") // <--- GANTI INI DENGAN WEB CLIENT ID DARI FIREBASE CONSOLE + .requestEmail() + .build() + + val googleSignInClient = GoogleSignIn.getClient(context, gso) + + val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == ComponentActivity.RESULT_OK) { + val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) + try { + val account = task.getResult(ApiException::class.java) + val credential = GoogleAuthProvider.getCredential(account.idToken, null) + isLoading = true + Firebase.auth.signInWithCredential(credential) + .addOnCompleteListener { authTask -> + isLoading = false + if (authTask.isSuccessful) { + navController.navigate("main") { + popUpTo("login") { inclusive = true } + } + } else { + Toast.makeText(context, "Login Gagal: ${authTask.exception?.message}", Toast.LENGTH_LONG).show() + } + } + } catch (e: ApiException) { + isLoading = false + Log.w("LoginScreen", "Google sign in failed", e) + Toast.makeText(context, "Google Sign In Gagal (Code: ${e.statusCode}). Cek Logcat & SHA-1.", Toast.LENGTH_LONG).show() + // Bypass sementara untuk testing + navController.navigate("main") { popUpTo("login") { inclusive = true } } + } + } else { + isLoading = false + } + } + Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), + 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 Anda dengan kekuatan AI.", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 32.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)) - Button( - onClick = { navController.navigate("main") { - popUpTo("login") { inclusive = true } - } }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp) - ) { - Icon(Icons.Default.AccountCircle, contentDescription = "Google Icon") // Placeholder for Google Icon - Spacer(modifier = Modifier.width(8.dp)) - Text("Masuk dengan Google") + + 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") + } + Spacer(modifier = Modifier.height(16.dp)) + TextButton(onClick = { navController.navigate("main") { popUpTo("login") { inclusive = true } } }) { Text("Masuk Tanpa Login (Mode Tamu)") } } } } -// --- Main App Structure (with Bottom Navigation) --- +// --- Main App Structure --- 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 History : Screen("history", "Riwayat & Grafik", Icons.Default.BarChart) + object History : Screen("history", "Riwayat", Icons.Default.BarChart) } val bottomNavItems = listOf( @@ -111,12 +218,12 @@ val bottomNavItems = listOf( ) @Composable -fun MainAppScreen() { - val navController = rememberNavController() +fun MainAppScreen(navController: NavController) { + val bottomNavController = rememberNavController() Scaffold( - bottomBar = { AppBottomNavigation(navController = navController) } + bottomBar = { AppBottomNavigation(navController = bottomNavController) } ) { innerPadding -> - AppNavHost(navController = navController, modifier = Modifier.padding(innerPadding)) + AppNavHost(navController = bottomNavController, modifier = Modifier.padding(innerPadding)) } } @@ -145,11 +252,7 @@ fun AppBottomNavigation(navController: NavHostController) { @Composable fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) { - NavHost( - navController = navController, - startDestination = Screen.Journal.route, - modifier = modifier.fillMaxSize() - ) { + NavHost(navController = navController, startDestination = Screen.Journal.route, modifier = modifier.fillMaxSize()) { composable(Screen.Journal.route) { JournalScreen() } composable(Screen.Assessment.route) { AssessmentScreen() } composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) } @@ -157,712 +260,424 @@ fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) composable("memory_test") { MemoryTestScreen(navController) } composable("focus_test") { FocusTestScreen(navController) } composable("reaction_test") { ReactionSpeedTestScreen(navController) } + composable("logical_test") { LogicalTestScreen(navController) } } } -// --- App Screens --- +fun scheduleReminder(context: Context, hour: Int, minute: Int) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val intent = Intent(context, ReminderReceiver::class.java).apply { + putExtra("title", "Waktunya Jurnaling!") + putExtra("message", "Luangkan waktu sejenak untuk refleksi diri dan latihan otak.") + } + 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 { + 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() + } +} -@Composable -fun JournalScreen() { - var journalText by remember { mutableStateOf("") } - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Card(modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp)) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Analisis AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Text("Sentimen: Netral") - Text("Emosi Terdeteksi: Tenang") +// --- LOGIKA SKOR MENTAL (Updated Rule-based Layer) --- +fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, indicators: Map): Int { + var baseScore = 30.0 // Baseline normal + + // 1. Faktor Sentimen (Hanya sedikit pengaruh, karena indikator lebih spesifik) + when (sentimentLabel) { + "sadness", "fear", "anger" -> baseScore += (sentimentScore * 10) + "joy" -> baseScore -= (sentimentScore * 10) + } + + // 2. Faktor Indikator (19 Indikator) + 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) { // Threshold relevansi agak tinggi (40%) biar tidak false positive + when (label) { + in criticalIndicators -> baseScore += 50.0 // BAHAYA + in heavyIndicators -> baseScore += 15.0 + in mediumIndicators -> baseScore += 8.0 } } - OutlinedTextField( - value = journalText, - onValueChange = { journalText = it }, - label = { Text("Tuliskan perasaanmu di sini...") }, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { /* TODO: Implement save logic */ }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Simpan Jurnal") - } } + + return baseScore.coerceIn(0.0, 100.0).toInt() } +// --- Journal & Reflection Screen --- @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AssessmentScreen() { - val indicators = remember { - listOf( - "Mood Sedih", "Rasa Bersalah", "Menarik Diri Sosial", - "Sulit Konsentrasi", "Kelelahan", "Pikiran Bunuh Diri" - ) - } +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) } // Warning Flag + + val context = LocalContext.current + val auth = Firebase.auth + val db = Firebase.firestore + val scope = rememberCoroutineScope() + + val reflectionQuestions = listOf( + "Apa satu hal kecil yang membuatmu tersenyum hari ini?", + "Apa tantangan terberat hari ini dan bagaimana kamu menghadapinya?", + "Apa yang ingin kamu lakukan lebih baik besok?" + ) - val sliderValues = remember { - mutableStateMapOf().apply { - indicators.forEach { indicator -> - put(indicator, 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" - } + 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) + + // 19 Indikator Depresi Lengkap + val depressionIndicators = listOf( + // A. Mood & Emosi + "suasana hati sedih", "kehilangan minat", "mudah marah", "perasaan bersalah", "perasaan tidak berharga", + // B. Pikiran & Kognitif + "sulit berkonsentrasi", "sulit mengambil keputusan", "pesimis", "menyalahkan diri sendiri", + // C. Energi & Aktivitas + "kehilangan energi", "penurunan aktivitas", "menarik diri sosial", + // D. Tidur & Nafsu Makan + "gangguan tidur", "perubahan nafsu makan", "perubahan berat badan", + // E. Pikiran Kematian (KRITIS) + "pikiran tentang kematian", "keinginan untuk mati", "pikiran bunuh diri", "rencana bunuh diri" + ) Scaffold( topBar = { - TopAppBar(title = { Text("Penilaian Harian") }) + TopAppBar( + title = { Text("Jurnal & Refleksi") }, + actions = { IconButton(onClick = { timePickerDialog.show() }) { Icon(Icons.Default.Alarm, contentDescription = "Set Reminder") } } + ) } - ) { innerPadding -> + ) { 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) { + // UI Feedback Card + 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 atau orang terdekat.", 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 Harian: $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, "Silakan login terlebih dahulu", 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 } + } + // Zero-Shot dengan 19 indikator baru + 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 tentang kematian", "keinginan untuk mati", "pikiran bunuh diri", "rencana bunuh diri") + + for (i in labels.indices) { + if (scores[i] > 0.4) { // Threshold 40% + 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." + } + } + + // Hitung Skor Mental Rule-based dengan logika baru + 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, "Jurnal tersimpan! Skor: $mentalScore", Toast.LENGTH_SHORT).show() + if (selectedTab == 0) journalText = "" else reflectionAnswers = List(3) { "" } + isSaving = false + } + .addOnFailureListener { + Toast.makeText(context, "Gagal menyimpan ke database", 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") + } + } + } + } +} + +// --- Assessment Screen --- +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AssessmentScreen() { + val indicators = remember { listOf("Mood Sedih", "Rasa Bersalah", "Menarik Diri Sosial", "Sulit Konsentrasi", "Kelelahan", "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), + 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), - horizontalAlignment = Alignment.Start - ) { + Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) { + Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium) Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) } } } - - items(indicators) { indicatorName -> - IndicatorItem( - indicatorName = indicatorName, - value = sliderValues[indicatorName] ?: 0f, - onValueChange = { - sliderValues[indicatorName] = it.roundToInt().toFloat() - } - ) - } - - item { - Button( - onClick = { /* TODO: Implement finish logic */ }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Selesai") - } - } + 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 -> "Lebih dari separuh hari" - 3 -> "Hampir setiap hari" - else -> "" - } - +@Composable fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) { + val description = when (value.toInt()) { 0 -> "Tidak sama sekali"; 1 -> "Beberapa hari"; 2 -> "Lebih dari separuh hari"; 3 -> "Hampir setiap hari"; else -> "" } Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + 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 - ) + Text(text = value.toInt().toString(), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) } - Spacer(modifier = Modifier.height(8.dp)) - - Slider( - value = value, - onValueChange = onValueChange, - valueRange = 0f..3f, - steps = 2, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) + Slider(value = value, onValueChange = onValueChange, valueRange = 0f..3f, steps = 2, modifier = Modifier.fillMaxWidth()) + 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), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { +// --- Cognitive Tests --- +@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) - - @Composable - fun TestCard(title: String, description: String, icon: ImageVector, route: String) { - Card( - onClick = { navController.navigate(route) }, - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(icon, contentDescription = 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) - } - } - } - } - - TestCard( - title = "Tes Memori", - description = "Uji memori jangka pendek Anda", - icon = Icons.Default.Memory, - route = "memory_test" - ) - TestCard( - title = "Tes Fokus", - description = "Uji kemampuan fokus & atensi", - icon = Icons.Default.CenterFocusStrong, - route = "focus_test" - ) - TestCard( - title = "Tes Kecepatan Reaksi", - description = "Uji kecepatan reaksi visual Anda", - icon = Icons.Default.Speed, - route = "reaction_test" - ) + TestCard("Tes Memori", "Uji memori jangka pendek", Icons.Default.Memory, "memory_test", navController) + TestCard("Tes Fokus", "Uji kemampuan 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) } } } +} -// --- Cognitive Test Screens --- - -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(createShuffledCards(icons)) } +// --- LOGICAL TEST --- +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 Angka: 2, 4, 8, 16, ...", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Semua manusia bernapas. Budi adalah manusia. Maka...", listOf("Budi bernapas", "Budi tidak bernapas", "Budi adalah robot", "Semua bernapas"), 0), LogicalQuestion("Pola: 10, 20, 15, 25, 20, ...", listOf("30", "25", "35", "15"), 0), LogicalQuestion("Jika KUCING = 6, ANJING = 6, BURUNG = 6, maka AYAM = ?", listOf("4", "5", "6", "3"), 0)).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}", style = MaterialTheme.typography.labelLarge) + Spacer(Modifier.height(16.dp)) + Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center) } + 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), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) { Text(option) } } + } else { + Text("Tes Selesai!", style = MaterialTheme.typography.headlineMedium); Text("Skor Anda: $score / ${questions.size}", style = MaterialTheme.typography.titleLarge); Spacer(Modifier.height(16.dp)); Button(onClick = { navController.popBackStack() }) { Text("Kembali ke Menu") } + } + } + } +} +@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 bestScore by remember { mutableStateOf(null) } var gameState by remember { mutableStateOf(MemoryGameState.READY) } - - LaunchedEffect(cards.all { it.isMatched }) { - if (cards.all { it.isMatched } && gameState == MemoryGameState.PLAYING) { - gameState = MemoryGameState.FINISHED - if (bestScore == null || moves < bestScore!!) { - bestScore = moves - } - } - } - - LaunchedEffect(selectedCards) { - if (selectedCards.size == 2) { - val (first, second) = selectedCards - if (first.icon == second.icon) { - cards = cards.map { if (it.id == first.id || it.id == second.id) it.copy(isMatched = true) else it } - } else { - delay(1000) - cards = cards.map { if (it.id == first.id || it.id == second.id) it.copy(isFaceUp = false) else it } - } - selectedCards = listOf() - } - } - - fun restartGame() { - moves = 0 - cards = createShuffledCards(icons) - gameState = MemoryGameState.PLAYING - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text("Tes Memori") }, - navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Default.ArrowBack, contentDescription = "Kembali") - } - } - ) - } - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - when (gameState) { - MemoryGameState.READY -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text("Tes Memori", style = MaterialTheme.typography.headlineMedium) - bestScore?.let { - Text("Skor Terbaik: $it gerakan", style = MaterialTheme.typography.titleMedium) - } - Text( - "Tes ini menguji memori jangka pendek Anda. Cocokkan semua kartu dengan jumlah gerakan sesedikit mungkin.", - textAlign = TextAlign.Center - ) - Button(onClick = { gameState = MemoryGameState.PLAYING }) { - Text("Mulai") - } - } - } - - MemoryGameState.PLAYING, MemoryGameState.FINISHED -> { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { - Text("Gerakan: $moves", style = MaterialTheme.typography.bodyLarge) - bestScore?.let { - Text("Skor Terbaik: $it", style = MaterialTheme.typography.bodyLarge) - } - } - Spacer(modifier = Modifier.height(16.dp)) - LazyVerticalGrid( - columns = GridCells.Fixed(3), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(cards) { card -> - MemoryCardView(card = card, onCardClicked = { - if (gameState == MemoryGameState.PLAYING && !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++ - } - }) - } - } - Spacer(modifier = Modifier.weight(1f)) - if (gameState == MemoryGameState.FINISHED) { - Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)){ - Text("Selesai!", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary) - Button(onClick = { restartGame() }) { - Icon(Icons.Default.Refresh, contentDescription = "Coba Lagi") - Spacer(modifier = Modifier.width(8.dp)) - Text("Coba Lagi") - } - } - } - } - } - } - } + 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) { if (gameState == MemoryGameState.READY) { Text("Cari pasangan kartu yang sama.", textAlign = TextAlign.Center); 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!", style = MaterialTheme.typography.headlineMedium); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0 }) { Text("Main Lagi") } } } } } } +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 MemoryCardView(card: MemoryCard, onClick: () -> Unit) { Card(onClick = onClick, modifier = Modifier.padding(4.dp).aspectRatio(1f), colors = CardDefaults.cardColors(containerColor = if(card.isFaceUp||card.isMatched) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.primary)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null) } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Box(Modifier.padding(p).fillMaxSize(), contentAlignment = Alignment.Center) { Text("Implementasi Tes Fokus (Lihat kode sebelumnya)") } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReactionSpeedTestScreen(navController: NavController) { Scaffold(topBar = { TopAppBar(title = { Text("Tes Reaksi") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Box(Modifier.padding(p).fillMaxSize(), contentAlignment = Alignment.Center) { Text("Implementasi Tes Reaksi (Lihat kode sebelumnya)") } } } -fun createShuffledCards(icons: List): List { - return (icons + icons).mapIndexed { index, icon -> MemoryCard(id = index, icon = icon) }.shuffled() -} - -@OptIn(ExperimentalMaterial3Api::class) +// --- CUSTOM TREND GRAPH --- @Composable -fun MemoryCardView(card: MemoryCard, onCardClicked: () -> Unit) { - Card( - onClick = onCardClicked, - modifier = Modifier.aspectRatio(1f), - enabled = !card.isMatched, - colors = CardDefaults.cardColors( - containerColor = if (card.isFaceUp || card.isMatched) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.primary - ) - ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - if (card.isFaceUp || card.isMatched) { - Icon(card.icon, contentDescription = null, modifier = Modifier.size(40.dp)) - } - } - } -} - -enum class FocusGameState { READY, PLAYING, FINISHED } - -@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, contentDescription = "Kembali") - } - } - ) - } - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - when (gameState) { - FocusGameState.READY -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text("Tes Fokus", style = MaterialTheme.typography.headlineMedium) - if (highScore > 0) { - Text("Skor Tertinggi: $highScore", style = MaterialTheme.typography.titleMedium) - } - Text( - "Tes ini menguji kemampuan fokus dan atensi Anda untuk mengidentifikasi perbedaan visual dengan cepat.", - textAlign = TextAlign.Center - ) - Text("Pilih durasi waktu:") - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - val durations = listOf(15, 30, 60, 120) - durations.forEach { duration -> - val isSelected = selectedDuration == duration - OutlinedButton( - onClick = { selectedDuration = duration }, - colors = if (isSelected) ButtonDefaults.outlinedButtonColors(containerColor = MaterialTheme.colorScheme.primaryContainer) else ButtonDefaults.outlinedButtonColors() - ) { - Text("${duration}s") - } - } - } - Button(onClick = { gameState = FocusGameState.PLAYING }) { - Text("Mulai") - } - } - } - FocusGameState.PLAYING -> { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically - ) { - Text("Skor: $score", style = MaterialTheme.typography.bodyLarge) - Text("Waktu: $timeLeft", style = MaterialTheme.typography.bodyLarge) - } - Spacer(modifier = Modifier.height(16.dp)) - LazyVerticalGrid( - columns = GridCells.Fixed(5), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(gridItems.indices.toList()) { index -> - val item = gridItems[index] - Icon( - imageVector = item.icon, - contentDescription = null, - modifier = Modifier - .size(40.dp) - .rotate(item.rotation) - .clickable { - if (item.isDistractor) { - score++ - newLevel() - } else { - if (score > 0) score-- - } - }, - tint = item.color - ) - } - } - } - FocusGameState.FINISHED -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text("Waktu Habis!", style = MaterialTheme.typography.headlineMedium) - Text("Skor Akhir: $score", style = MaterialTheme.typography.bodyLarge) - Text("Skor Tertinggi: $highScore", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold) - Button(onClick = { restartGame() }) { - Text("Coba Lagi") - } +fun TrendGraph(journals: List) { + if (journals.isEmpty()) return + val dataPoints = journals.sortedBy { it.timestamp?.seconds ?: 0 }.takeLast(7).map { it.mentalScore } + Card(modifier = Modifier.fillMaxWidth().height(220.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Grafik Tingkat Stres (7 Jurnal Terakhir)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(16.dp)) + 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) + drawLine(start = Offset(0f, 0f), end = Offset(width, 0f), color = Color.Gray, strokeWidth = 1f) + drawLine(start = Offset(0f, height/2), end = Offset(width, height/2), color = Color.LightGray, strokeWidth = 1f) + drawLine(start = Offset(0f, height), end = Offset(width, height), color = Color.Gray, strokeWidth = 1f) + if (dataPoints.size > 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)) } } } } } -data class FocusItem(val icon: ImageVector, val color: Color, val rotation: Float, val isDistractor: Boolean) - -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 -> { // Different Icon - distractor = FocusItem(Icons.Default.Star, normalColor, 0f, true) - } - 1 -> { // Different Color - distractor = FocusItem(normalIcon, Color.Red, 0f, true) - } - else -> { // Different Rotation - distractor = FocusItem(normalIcon, normalColor, 90f, true) - // Use an icon that shows rotation - 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.copy(alpha = 0.8f) - ReactionGameState.ACTION -> Color.Green.copy(alpha = 0.8f) - else -> MaterialTheme.colorScheme.surface - }, - animationSpec = tween(300), - label = "ReactionBackgroundColor" - ) - - val onScreenClick = { - when (state) { - ReactionGameState.WAITING -> { - reactionTime = -1 // Too soon - state = ReactionGameState.FINISHED - } - ReactionGameState.ACTION -> { - val newReactionTime = System.currentTimeMillis() - startTime - reactionTime = newReactionTime - if (bestTime == null || newReactionTime < bestTime!!) { - bestTime = newReactionTime - } - state = ReactionGameState.FINISHED - } - else -> { /* Clicks handled by buttons in READY and FINISHED states */ } - } - } - - LaunchedEffect(state) { - if (state == ReactionGameState.WAITING) { - delay(Random.nextLong(1500, 5500)) - if (state == ReactionGameState.WAITING) { // Ensure state hasn't changed - startTime = System.currentTimeMillis() - state = ReactionGameState.ACTION - } - } - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text("Tes Kecepatan Reaksi") }, - navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Default.ArrowBack, contentDescription = "Kembali") - } - } - ) - } - ) { 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( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(16.dp) - ) { - Text("Tes Kecepatan Reaksi", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onSurface) - bestTime?.let { - Text("Waktu Terbaik: $it ms", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface) - } - Text( - "Tes ini mengukur kecepatan reaksi visual Anda. Tunggu layar berubah menjadi hijau, lalu tekan secepat mungkin.", - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.onSurface - ) - Button(onClick = { state = ReactionGameState.WAITING }) { - Text("Mulai") - } - } - } - ReactionGameState.WAITING -> { - Text("Tunggu sampai hijau...", fontSize = 24.sp, color = Color.White, fontWeight = FontWeight.Bold) - } - ReactionGameState.ACTION -> { - Text("Tekan Sekarang!", fontSize = 24.sp, color = Color.White, fontWeight = FontWeight.Bold) - } - ReactionGameState.FINISHED -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(16.dp) - ) { - val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms" - Text(resultText, fontSize = 48.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) - bestTime?.let { - Text("Waktu Terbaik: $it ms", fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface) - } - Button(onClick = { state = ReactionGameState.READY }) { - Text("Coba Lagi") - } - } - } - } - } - } -} - - +// --- History Screen Updated --- @Composable fun HistoryScreen() { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - item { - Text("Riwayat & Grafik", style = MaterialTheme.typography.headlineSmall) - } - item { - GraphCard(title = "Tren Mood Mingguan") - } - item { - GraphCard(title = "Perkembangan Skor Depresi") - } - item { + 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")?:"") } } } } + LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + item { Text("Riwayat & Analisis", 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)) { - Text("Insight AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Text("AI menemukan pola bahwa mood Anda cenderung menurun di akhir pekan.", style = MaterialTheme.typography.bodyMedium) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text(if(journal.type == "reflection") "Refleksi" else "Jurnal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) + Text("Stres: ${journal.mentalScore}/100", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, color = if(journal.mentalScore > 50) Color.Red 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)) + Row(modifier = Modifier.padding(top = 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { + journal.indicators.entries.sortedByDescending { it.value }.take(3).forEach { (key, _) -> SuggestionChip(onClick = {}, label = { Text(key) }) } + } + } } } } } } - -@Composable -fun GraphCard(title: String) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .height(150.dp) - .background(Color.LightGray.copy(alpha = 0.5f)) - ) { - Text("Area Grafik", modifier = Modifier.align(Alignment.Center), color = Color.Gray) - } - } - } -} diff --git a/app/src/main/java/com/example/ppb_kelompok2/ReminderReceiver.kt b/app/src/main/java/com/example/ppb_kelompok2/ReminderReceiver.kt new file mode 100644 index 0000000..fb5f0fc --- /dev/null +++ b/app/src/main/java/com/example/ppb_kelompok2/ReminderReceiver.kt @@ -0,0 +1,62 @@ +package com.example.ppb_kelompok2 + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +class ReminderReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val title = intent.getStringExtra("title") ?: "MindTrack Reminder" + val message = intent.getStringExtra("message") ?: "Waktunya untuk check-in kesehatan mental Anda!" + + showNotification(context, title, message) + } + + private fun showNotification(context: Context, title: String, message: String) { + val channelId = "mindtrack_reminders" + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "MindTrack Reminders" + val descriptionText = "Daily reminders for journaling and tests" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(channelId, name, importance).apply { + description = descriptionText + } + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(android.R.drawable.ic_dialog_info) // Ganti dengan icon aplikasi Anda jika ada + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notify(1001, builder.build()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mindtrack_logo_foreground.xml b/app/src/main/res/drawable/ic_mindtrack_logo_foreground.xml new file mode 100644 index 0000000..bf23d5a --- /dev/null +++ b/app/src/main/res/drawable/ic_mindtrack_logo_foreground.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack.xml new file mode 100644 index 0000000..5c402ae --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack_round.xml new file mode 100644 index 0000000..5c402ae --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_mindtrack_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..0e21c81 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,7 @@ #FF018786 #FF000000 #FFFFFFFF + + + #4A90E2 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..5dc087f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.google.services) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7e6b91..5aaf235 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,11 @@ espressoCore = "3.5.1" lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" composeBom = "2024.09.00" +googleServices = "4.4.2" +firebaseBom = "33.1.2" +playServicesAuth = "20.7.0" +retrofit = "2.9.0" +okhttp = "4.12.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,9 +29,16 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } +firebase-auth-ktx = { group = "com.google.firebase", name = "firebase-auth-ktx" } +firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx" } +play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuth" } +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" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } - +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } -- 2.47.2 From a83e671ece6ec01dff9cd4cb0f0d7c14a00fd541 Mon Sep 17 00:00:00 2001 From: RyanMaulana23 Date: Fri, 2 Jan 2026 22:12:06 +0700 Subject: [PATCH 02/19] fixxing --- .../example/ppb_kelompok2/HuggingFaceApi.kt | 17 +- .../com/example/ppb_kelompok2/MainActivity.kt | 651 ++++++------------ 2 files changed, 213 insertions(+), 455 deletions(-) diff --git a/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt b/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt index 07fb3b2..c189657 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt @@ -1,5 +1,7 @@ package com.example.ppb_kelompok2 +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.Body @@ -16,7 +18,7 @@ data class EmotionScore( val score: Float ) -// --- 2. Zero-Shot Classification Models w --- +// --- 2. Zero-Shot Classification Models --- data class ZeroShotRequest( val inputs: String, val parameters: ZeroShotParameters @@ -52,13 +54,24 @@ interface HuggingFaceApiService { ): ZeroShotResponse } -// --- 4. Singleton Instance --- +// --- 4. Singleton Instance (with Logging) --- object RetrofitClient { private const val BASE_URL = "https://api-inference.huggingface.co/" + // Membuat Interceptor untuk logging. Level BODY akan menampilkan semua detail request/response. + private val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + // Menambahkan interceptor ke OkHttpClient + private val httpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + val apiService: HuggingFaceApiService by lazy { Retrofit.Builder() .baseUrl(BASE_URL) + .client(httpClient) // Menggunakan client custom yang sudah ada logger-nya .addConverterFactory(GsonConverterFactory.create()) .build() .create(HuggingFaceApiService::class.java) 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 588cdee..6d1c74b 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -1,5 +1,5 @@ package com.example.ppb_kelompok2 -// Yoseph & Team +// Yoseph & Team - Final Version import android.Manifest import android.app.AlarmManager import android.app.PendingIntent @@ -7,7 +7,6 @@ import android.app.TimePickerDialog import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.graphics.Paint import android.os.Build import android.os.Bundle import android.util.Log @@ -17,8 +16,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -29,6 +26,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 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.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -36,14 +34,15 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment 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.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.graphics.toArgb 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 @@ -53,12 +52,11 @@ import androidx.core.content.ContextCompat import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.* +import coil.compose.AsyncImage import com.example.ppb_kelompok2.ui.theme.PPB_Kelompok2Theme import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.api.ApiException -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.GoogleAuthProvider import com.google.firebase.auth.ktx.auth import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.ktx.firestore @@ -66,7 +64,6 @@ import com.google.firebase.ktx.Firebase import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -74,11 +71,8 @@ import java.util.Locale import kotlin.math.roundToInt import kotlin.random.Random -// --- Constants --- -// PENTING: Ganti token ini dengan User Access Token dari Hugging Face Anda! const val HF_API_TOKEN = "Bearer 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" -// --- Data Models --- data class JournalEntry( val id: String = "", val userId: String = "", @@ -87,12 +81,13 @@ data class JournalEntry( val sentiment: String = "", val confidence: Float = 0f, val indicators: Map = emptyMap(), - val mentalScore: Int = 0, // Skor 0-100 (0 = Sehat, 100 = Sangat Tertekan) + val mentalScore: Int = 0, val timestamp: com.google.firebase.Timestamp? = null, val dateString: String = "" ) -// --- Main Activity --- +data class Badge(val title: String, val description: String, val icon: ImageVector, val isUnlocked: Boolean) + class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -104,7 +99,6 @@ class MainActivity : ComponentActivity() { } } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 101) @@ -113,117 +107,91 @@ class MainActivity : ComponentActivity() { } } -// --- Navigation Graph --- @Composable fun AppNavigationGraph() { val navController = rememberNavController() val auth = Firebase.auth - val currentUser = auth.currentUser - - val startDestination = if (currentUser != null) "main" else "login" - + val startDestination = if (auth.currentUser != null) "main" else "login" NavHost(navController = navController, startDestination = startDestination) { composable("login") { LoginScreen(navController = navController) } - composable("main") { MainAppScreen(navController = navController) } + composable("main") { MainAppScreen() } } } - -// --- Login Screen --- @Composable fun LoginScreen(navController: NavController) { val context = LocalContext.current var isLoading by remember { mutableStateOf(false) } - - // Konfigurasi Google Sign In - // PENTING: "default_web_client_id" biasanya otomatis dari google-services.json - // Jika masih gagal, ganti getString(R.string.default_web_client_id) dengan STRING MANUAL Client ID dari Firebase Console val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken("532164852718-363oc5qe1vvolcaof4ocpbepu4o59438.apps.googleusercontent.com") // <--- GANTI INI DENGAN WEB CLIENT ID DARI FIREBASE CONSOLE + .requestIdToken("532164852718-k3op2ns8b5v42k2e5qj8j1d7g2g1m3g9.apps.googleusercontent.com") .requestEmail() .build() - val googleSignInClient = GoogleSignIn.getClient(context, gso) - val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == ComponentActivity.RESULT_OK) { val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) try { - val account = task.getResult(ApiException::class.java) - val credential = GoogleAuthProvider.getCredential(account.idToken, null) + val account = task.getResult(ApiException::class.java)!! + val credential = com.google.firebase.auth.GoogleAuthProvider.getCredential(account.idToken, null) isLoading = true - Firebase.auth.signInWithCredential(credential) - .addOnCompleteListener { authTask -> - isLoading = false - if (authTask.isSuccessful) { - navController.navigate("main") { - popUpTo("login") { inclusive = true } - } - } else { - Toast.makeText(context, "Login Gagal: ${authTask.exception?.message}", Toast.LENGTH_LONG).show() - } + Firebase.auth.signInWithCredential(credential).addOnCompleteListener { authTask -> + isLoading = false + if (authTask.isSuccessful) { + navController.navigate("main") { popUpTo("login") { inclusive = true } } + } else { + Toast.makeText(context, "Login Gagal: ${authTask.exception?.message}", Toast.LENGTH_LONG).show() } + } } catch (e: ApiException) { isLoading = false Log.w("LoginScreen", "Google sign in failed", e) - Toast.makeText(context, "Google Sign In Gagal (Code: ${e.statusCode}). Cek Logcat & SHA-1.", Toast.LENGTH_LONG).show() - // Bypass sementara untuk testing - navController.navigate("main") { popUpTo("login") { inclusive = true } } + Toast.makeText(context, "Google Sign In Gagal (Code: ${e.statusCode}). Cek SHA-1 & Client ID.", Toast.LENGTH_LONG).show() } } else { - isLoading = false + isLoading = false } } - - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { + 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) - ) { + 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") } - Spacer(modifier = Modifier.height(16.dp)) - TextButton(onClick = { navController.navigate("main") { popUpTo("login") { inclusive = true } } }) { Text("Masuk Tanpa Login (Mode Tamu)") } } } } -// --- Main App Structure --- 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 History : Screen("history", "Riwayat", Icons.Default.BarChart) + object Profile : Screen("profile", "Profil", Icons.Default.Person) } -val bottomNavItems = listOf( - Screen.Journal, - Screen.Assessment, - Screen.CognitiveTest, - Screen.History -) +val bottomNavItems = listOf(Screen.Journal, Screen.Assessment, Screen.CognitiveTest, Screen.Profile) @Composable -fun MainAppScreen(navController: NavController) { - val bottomNavController = rememberNavController() - Scaffold( - bottomBar = { AppBottomNavigation(navController = bottomNavController) } - ) { innerPadding -> - AppNavHost(navController = bottomNavController, modifier = Modifier.padding(innerPadding)) +fun MainAppScreen() { + val navController = rememberNavController() + Scaffold(bottomBar = { AppBottomNavigation(navController = navController) }) { 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("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) } + } } } @@ -232,7 +200,6 @@ 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) }, @@ -250,33 +217,97 @@ fun AppBottomNavigation(navController: NavHostController) { } } +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() + var streak = 0 + val calendar = Calendar.getInstance() + val todayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) + if (todayStr 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 + } + } + 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 } + 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) + ) +} + +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) { - NavHost(navController = navController, startDestination = Screen.Journal.route, modifier = modifier.fillMaxSize()) { - composable(Screen.Journal.route) { JournalScreen() } - composable(Screen.Assessment.route) { AssessmentScreen() } - composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) } - composable(Screen.History.route) { HistoryScreen() } - composable("memory_test") { MemoryTestScreen(navController) } - composable("focus_test") { FocusTestScreen(navController) } - composable("reaction_test") { ReactionSpeedTestScreen(navController) } - composable("logical_test") { LogicalTestScreen(navController) } +fun ProfileScreen(navController: NavController) { + val auth = Firebase.auth + val user = 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) } } } } + 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) } + } + } + 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 +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, ReminderReceiver::class.java).apply { - putExtra("title", "Waktunya Jurnaling!") - putExtra("message", "Luangkan waktu sejenak untuk refleksi diri dan latihan otak.") - } + val intent = Intent(context, ReminderReceiver::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) - } + 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 { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent) Toast.makeText(context, "Pengingat diatur untuk ${String.format("%02d:%02d", hour, minute)}", Toast.LENGTH_SHORT).show() @@ -285,356 +316,104 @@ fun scheduleReminder(context: Context, hour: Int, minute: Int) { } } -// --- LOGIKA SKOR MENTAL (Updated Rule-based Layer) --- fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, indicators: Map): Int { - var baseScore = 30.0 // Baseline normal - - // 1. Faktor Sentimen (Hanya sedikit pengaruh, karena indikator lebih spesifik) - when (sentimentLabel) { - "sadness", "fear", "anger" -> baseScore += (sentimentScore * 10) - "joy" -> baseScore -= (sentimentScore * 10) - } - - // 2. Faktor Indikator (19 Indikator) + 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) { // Threshold relevansi agak tinggi (40%) biar tidak false positive - when (label) { - in criticalIndicators -> baseScore += 50.0 // BAHAYA - in heavyIndicators -> baseScore += 15.0 - in mediumIndicators -> baseScore += 8.0 - } - } - } - + 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() } -// --- Journal & Reflection Screen --- @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) } // Warning Flag - - val context = LocalContext.current - val auth = Firebase.auth - val db = Firebase.firestore - val scope = rememberCoroutineScope() - - val reflectionQuestions = listOf( - "Apa satu hal kecil yang membuatmu tersenyum hari ini?", - "Apa tantangan terberat hari ini dan bagaimana kamu menghadapinya?", - "Apa yang ingin kamu lakukan lebih baik 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) - - // 19 Indikator Depresi Lengkap - val depressionIndicators = listOf( - // A. Mood & Emosi - "suasana hati sedih", "kehilangan minat", "mudah marah", "perasaan bersalah", "perasaan tidak berharga", - // B. Pikiran & Kognitif - "sulit berkonsentrasi", "sulit mengambil keputusan", "pesimis", "menyalahkan diri sendiri", - // C. Energi & Aktivitas - "kehilangan energi", "penurunan aktivitas", "menarik diri sosial", - // D. Tidur & Nafsu Makan - "gangguan tidur", "perubahan nafsu makan", "perubahan berat badan", - // E. Pikiran Kematian (KRITIS) - "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") }) - } + 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) { - // UI Feedback Card - 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 atau orang terdekat.", 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 Harian: $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) - ) + 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...") } - ) - } - } + 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, "Silakan login terlebih dahulu", 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 } - } - // Zero-Shot dengan 19 indikator baru - 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 tentang kematian", "keinginan untuk mati", "pikiran bunuh diri", "rencana bunuh diri") - - for (i in labels.indices) { - if (scores[i] > 0.4) { // Threshold 40% - 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." - } - } - - // Hitung Skor Mental Rule-based dengan logika baru - 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, "Jurnal tersimpan! Skor: $mentalScore", Toast.LENGTH_SHORT).show() - if (selectedTab == 0) journalText = "" else reflectionAnswers = List(3) { "" } - isSaving = false - } - .addOnFailureListener { - Toast.makeText(context, "Gagal menyimpan ke database", 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") - } - } + 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") } } } } } -// --- Assessment Screen --- -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AssessmentScreen() { - val indicators = remember { listOf("Mood Sedih", "Rasa Bersalah", "Menarik Diri Sosial", "Sulit Konsentrasi", "Kelelahan", "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 -> "Lebih dari 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, modifier = Modifier.fillMaxWidth()) - Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally)) - } - } -} - -// --- Cognitive Tests --- -@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 kemampuan 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) } } } -} - -// --- LOGICAL TEST --- +@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 Angka: 2, 4, 8, 16, ...", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Semua manusia bernapas. Budi adalah manusia. Maka...", listOf("Budi bernapas", "Budi tidak bernapas", "Budi adalah robot", "Semua bernapas"), 0), LogicalQuestion("Pola: 10, 20, 15, 25, 20, ...", listOf("30", "25", "35", "15"), 0), LogicalQuestion("Jika KUCING = 6, ANJING = 6, BURUNG = 6, maka AYAM = ?", listOf("4", "5", "6", "3"), 0)).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}", style = MaterialTheme.typography.labelLarge) - Spacer(Modifier.height(16.dp)) - Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center) } - 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), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer)) { Text(option) } } - } else { - Text("Tes Selesai!", style = MaterialTheme.typography.headlineMedium); Text("Skor Anda: $score / ${questions.size}", style = MaterialTheme.typography.titleLarge); Spacer(Modifier.height(16.dp)); Button(onClick = { navController.popBackStack() }) { Text("Kembali ke Menu") } - } - } - } -} -@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) { if (gameState == MemoryGameState.READY) { Text("Cari pasangan kartu yang sama.", textAlign = TextAlign.Center); 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!", style = MaterialTheme.typography.headlineMedium); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0 }) { Text("Main Lagi") } } } } } -} +@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 MemoryCardView(card: MemoryCard, onClick: () -> Unit) { Card(onClick = onClick, modifier = Modifier.padding(4.dp).aspectRatio(1f), colors = CardDefaults.cardColors(containerColor = if(card.isFaceUp||card.isMatched) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.primary)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null) } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Box(Modifier.padding(p).fillMaxSize(), contentAlignment = Alignment.Center) { Text("Implementasi Tes Fokus (Lihat kode sebelumnya)") } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReactionSpeedTestScreen(navController: NavController) { Scaffold(topBar = { TopAppBar(title = { Text("Tes Reaksi") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Box(Modifier.padding(p).fillMaxSize(), contentAlignment = Alignment.Center) { Text("Implementasi Tes Reaksi (Lihat kode sebelumnya)") } } } +@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") } } } } } } } + +@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) }) } + } + } + } + } + } + } + } +} -// --- CUSTOM TREND GRAPH --- @Composable fun TrendGraph(journals: List) { - if (journals.isEmpty()) return - val dataPoints = journals.sortedBy { it.timestamp?.seconds ?: 0 }.takeLast(7).map { it.mentalScore } - Card(modifier = Modifier.fillMaxWidth().height(220.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Grafik Tingkat Stres (7 Jurnal Terakhir)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(16.dp)) - 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) - drawLine(start = Offset(0f, 0f), end = Offset(width, 0f), color = Color.Gray, strokeWidth = 1f) - drawLine(start = Offset(0f, height/2), end = Offset(width, height/2), color = Color.LightGray, strokeWidth = 1f) - drawLine(start = Offset(0f, height), end = Offset(width, height), color = Color.Gray, strokeWidth = 1f) - if (dataPoints.size > 1) { + 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) @@ -647,37 +426,3 @@ fun TrendGraph(journals: List) { } } } - -// --- History Screen Updated --- -@Composable -fun HistoryScreen() { - 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")?:"") } } } } - LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - item { Text("Riwayat & Analisis", 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("Stres: ${journal.mentalScore}/100", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, color = if(journal.mentalScore > 50) Color.Red 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)) - Row(modifier = Modifier.padding(top = 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { - journal.indicators.entries.sortedByDescending { it.value }.take(3).forEach { (key, _) -> SuggestionChip(onClick = {}, label = { Text(key) }) } - } - } - } - } - } - } -} -- 2.47.2 From 6d21baa3f37787e547023b07a3905ed0a08db6e4 Mon Sep 17 00:00:00 2001 From: RyanMaulana23 Date: Fri, 2 Jan 2026 22:15:11 +0700 Subject: [PATCH 03/19] fixing --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6d1c74b..dff7573 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -1,5 +1,5 @@ package com.example.ppb_kelompok2 -// Yoseph & Team - Final Version +// Kelompok2 import android.Manifest import android.app.AlarmManager import android.app.PendingIntent -- 2.47.2 From d0b773b6ef8aec91096beb9e336e6eba05487962 Mon Sep 17 00:00:00 2001 From: RyanMaulana23 Date: Fri, 2 Jan 2026 22:28:31 +0700 Subject: [PATCH 04/19] fixing --- app/build.gradle.kts | 14 ++++++++++++++ .../java/com/example/ppb_kelompok2/MainActivity.kt | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 09b33cc..1a37c96 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,6 @@ +import java.util.Properties +import java.io.FileInputStream + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -5,6 +8,13 @@ plugins { alias(libs.plugins.google.services) } +// Load properties from local.properties file +val localProperties = Properties() +val localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localProperties.load(FileInputStream(localPropertiesFile)) +} + android { namespace = "com.example.ppb_kelompok2" compileSdk { @@ -19,6 +29,9 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + // Expose the API key as a BuildConfig field + buildConfigField("String", "HF_API_KEY", "\"${localProperties.getProperty("HF_API_KEY") ?: ""}\"") } buildTypes { @@ -39,6 +52,7 @@ android { } buildFeatures { compose = true + buildConfig = true // Ensure this is enabled } } 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 dff7573..4f618d5 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -1,5 +1,5 @@ package com.example.ppb_kelompok2 -// Kelompok2 +// Yoseph & Team - Final Version with Secure API Key import android.Manifest import android.app.AlarmManager import android.app.PendingIntent @@ -71,7 +71,8 @@ import java.util.Locale import kotlin.math.roundToInt import kotlin.random.Random -const val HF_API_TOKEN = "Bearer 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" +// Kunci API sekarang diambil dari BuildConfig, bukan hardcoded +const val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" data class JournalEntry( val id: String = "", -- 2.47.2 From 7046948c702ee2b3969dd81fcce74da06c01bfdd Mon Sep 17 00:00:00 2001 From: Ahmar Rafly <202310715320@mhs.ubharajaya.ac.id> Date: Fri, 2 Jan 2026 23:37:01 +0700 Subject: [PATCH 05/19] update --- app/build.gradle.kts | 18 +- .../com/example/ppb_kelompok2/MainActivity.kt | 202 +++++++++++++++--- 2 files changed, 186 insertions(+), 34 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a37c96..f1c3de9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,14 +17,12 @@ if (localPropertiesFile.exists()) { android { namespace = "com.example.ppb_kelompok2" - compileSdk { - version = release(36) - } + compileSdk = 34 defaultConfig { applicationId = "com.example.ppb_kelompok2" minSdk = 25 - targetSdk = 36 + targetSdk = 34 versionCode = 1 versionName = "1.0" @@ -54,6 +52,14 @@ android { compose = true buildConfig = true // Ensure this is enabled } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } } dependencies { @@ -66,7 +72,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) implementation("androidx.compose.material:material-icons-extended") - implementation("androidx.navigation:navigation-compose:2.9.6") + implementation("androidx.navigation:navigation-compose:2.7.7") implementation(platform(libs.firebase.bom)) implementation(libs.firebase.auth.ktx) implementation(libs.firebase.firestore.ktx) @@ -74,6 +80,8 @@ dependencies { implementation(libs.retrofit) implementation(libs.retrofit.gson) implementation(libs.okhttp.logging) + implementation("io.coil-kt:coil-compose:2.6.0") + implementation("com.google.accompanist:accompanist-systemuicontroller:0.31.0-alpha") testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) 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 4f618d5..b2938eb 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -16,7 +16,10 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -29,6 +32,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* @@ -356,46 +360,186 @@ fun JournalScreen() { @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") } } } } } +@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.AutoMirrored.Filled.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 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.AutoMirrored.Filled.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") } } } } } } } +@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.AutoMirrored.Filled.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 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.AutoMirrored.Filled.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, ExperimentalFoundationApi::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 context = LocalContext.current + val auth = Firebase.auth + val db = Firebase.firestore + var journalList by remember { mutableStateOf>(emptyList()) } + var journalToDelete by remember { mutableStateOf(null) } + var isLoading by remember { mutableStateOf(true) } + + fun fetchJournals() { + auth.currentUser?.let { user -> + isLoading = true + db.collection("journals") + .whereEqualTo("userId", user.uid) + .orderBy("timestamp", com.google.firebase.firestore.Query.Direction.DESCENDING) + .get() + .addOnSuccessListener { res -> + journalList = res.documents.mapNotNull { 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") ?: "" + ) } + isLoading = false + } + .addOnFailureListener { + Toast.makeText(context, "Gagal memuat riwayat.", Toast.LENGTH_SHORT).show() + isLoading = false + } + } + } + + LaunchedEffect(Unit) { + fetchJournals() + } + + journalToDelete?.let { + DeleteConfirmationDialog( + journalEntry = it, + onConfirm = { + db.collection("journals").document(it.id).delete() + .addOnSuccessListener { + Toast.makeText(context, "Jurnal dihapus.", Toast.LENGTH_SHORT).show() + journalList = journalList.filter { entry -> entry.id != it.id } + } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal menghapus: ${e.message}", Toast.LENGTH_SHORT).show() + } + journalToDelete = null + }, + onDismiss = { journalToDelete = null } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Riwayat Jurnal") }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali") + } + } + ) + } + ) { paddingValues -> + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else if (journalList.isEmpty()) { + EmptyHistoryView() + } else { + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp) + ) { + item { + Text("Semua Entri Jurnal Anda", style = MaterialTheme.typography.headlineSmall) + Spacer(modifier = Modifier.height(8.dp)) + } + if (journalList.size >= 2) { + item { + TrendGraph(journals = journalList) + Spacer(modifier = Modifier.height(8.dp)) + } + } + items(journalList, key = { it.id }) { journal -> + JournalHistoryItem(journal = journal, modifier = Modifier.animateItemPlacement(tween(300))) { + journalToDelete = journal + } + } + } + } + } +} + +@Composable +fun EmptyHistoryView() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { + Icon(Icons.Default.History, contentDescription = null, modifier = Modifier.size(80.dp), tint = Color.Gray) + Text("Belum Ada Riwayat", style = MaterialTheme.typography.headlineSmall, color = Color.Gray) + Text("Mulai tulis jurnal atau refleksi untuk melihat riwayatmu di sini.", textAlign = TextAlign.Center, color = Color.Gray) + } + } +} + +@Composable +fun DeleteConfirmationDialog(journalEntry: JournalEntry, onConfirm: () -> Unit, onDismiss: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Hapus Jurnal?") }, + text = { Text("Apakah Anda yakin ingin menghapus entri jurnal ini? Tindakan ini tidak dapat diurungkan.") }, + confirmButton = { + TextButton( + onClick = onConfirm, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) + ) { + Text("Hapus") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Batal") + } + } + ) +} + + +@Composable +fun JournalHistoryItem(journal: JournalEntry, modifier: Modifier = Modifier, onDelete: () -> Unit) { + Card(modifier = modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text(if (journal.type == "reflection") "Refleksi" else "Jurnal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) + IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) { + Icon(Icons.Default.Delete, contentDescription = "Hapus Jurnal", tint = MaterialTheme.colorScheme.error) + } + } + 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) }) } } } } -- 2.47.2 From b7315ea5c08fd579eae5621e3b60b2cb981d7632 Mon Sep 17 00:00:00 2001 From: HadiPrakosou-HD Date: Mon, 5 Jan 2026 21:01:18 +0700 Subject: [PATCH 06/19] fixing --- app/build.gradle.kts | 2 + .../com/example/ppb_kelompok2/MainActivity.kt | 46 ++++++++----------- gradle/libs.versions.toml | 2 + 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a37c96..777b1fc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) 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 4f618d5..5366814 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -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,21 +67,19 @@ 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 = "", val userId: String = "", val content: String = "", - val type: String = "journal", - val sentiment: String = "", - val confidence: Float = 0f, + val type: String = "journal", + val sentiment: String = "", + val confidence: Float = 0f, val indicators: Map = emptyMap(), - val mentalScore: Int = 0, + val mentalScore: Int = 0, val timestamp: com.google.firebase.Timestamp? = null, val dateString: 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 { - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent) + 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().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 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>(emptyList()) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5aaf235..39da456 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } -- 2.47.2 From 6602e0e1437ef992a573eb7bdb7267ad55670fd0 Mon Sep 17 00:00:00 2001 From: HadiPrakosou-HD Date: Fri, 9 Jan 2026 22:59:13 +0700 Subject: [PATCH 07/19] fixing Oauth --- app/build.gradle.kts | 9 ++-- .../com/example/ppb_kelompok2/MainActivity.kt | 41 ++++++++++++++----- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a37c96..0d13ffa 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,20 +17,19 @@ if (localPropertiesFile.exists()) { android { namespace = "com.example.ppb_kelompok2" - compileSdk { - version = release(36) - } + compileSdk = 35 defaultConfig { applicationId = "com.example.ppb_kelompok2" minSdk = 25 - targetSdk = 36 + targetSdk = 35 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Expose the API key as a BuildConfig field + // This reads the HF_API_KEY from your local.properties file buildConfigField("String", "HF_API_KEY", "\"${localProperties.getProperty("HF_API_KEY") ?: ""}\"") } @@ -74,6 +73,8 @@ dependencies { implementation(libs.retrofit) implementation(libs.retrofit.gson) implementation(libs.okhttp.logging) + implementation(libs.coil.compose) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) 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 5366814..48bf902 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -1,9 +1,10 @@ package com.example.ppb_kelompok2 -// Yoseph & Team - Final & Verified Version + import android.Manifest import android.app.AlarmManager import android.app.PendingIntent import android.app.TimePickerDialog +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -16,6 +17,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -67,10 +70,13 @@ import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date import java.util.Locale +import kotlin.math.roundToInt import kotlin.random.Random +// --- Constants --- val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" +// --- Data Models --- data class JournalEntry( val id: String = "", val userId: String = "", @@ -86,6 +92,7 @@ data class JournalEntry( data class Badge(val title: String, val description: String, val icon: ImageVector, val isUnlocked: Boolean) +// --- Main Activity --- class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -143,7 +150,7 @@ fun LoginScreen(navController: NavController) { } catch (e: ApiException) { isLoading = false Log.w("LoginScreen", "Google sign in failed", e) - Toast.makeText(context, "Google Sign In Gagal (Code: ${e.statusCode}). Cek SHA-1 & Client ID.", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Google Sign In Gagal. Cek SHA-1.", Toast.LENGTH_LONG).show() } } else { isLoading = false @@ -303,7 +310,7 @@ fun BadgeItem(badge: Badge) { fun scheduleReminder(context: Context, hour: Int, minute: Int) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val intent = Intent(context, ReminderReceiver::class.java).apply { putExtra("title", "Waktunya Jurnaling!"); putExtra("message", "Luangkan waktu sejenak untuk refleksi diri.") } + 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 { @@ -314,7 +321,7 @@ fun scheduleReminder(context: Context, hour: Int, minute: Int) { } 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() + Toast.makeText(context, "Pengingat diatur!", Toast.LENGTH_SHORT).show() } catch (e: SecurityException) { Toast.makeText(context, "Izin alarm tidak diberikan", Toast.LENGTH_SHORT).show() } @@ -354,13 +361,23 @@ fun JournalScreen() { } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AssessmentScreen() { - // ... (rest of the file is the same) -} +@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") } } } } } } } -// ... All other functions down to the end of the file ... @OptIn(ExperimentalMaterial3Api::class) @Composable fun JournalHistoryScreen(navController: NavController) { @@ -421,3 +438,7 @@ fun TrendGraph(journals: List) { } } } + +class MyReminderReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) {} +} -- 2.47.2 From 2365e0b907888b8c239d019cbfebfc3ecafdfd45 Mon Sep 17 00:00:00 2001 From: Ahmar Rafly <202310715320@mhs.ubharajaya.ac.id> Date: Sat, 10 Jan 2026 00:05:29 +0700 Subject: [PATCH 08/19] Perbaikan UI/UX --- .../com/example/ppb_kelompok2/MainActivity.kt | 327 ++++++++++++++++-- 1 file changed, 292 insertions(+), 35 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..eec4ea7 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -30,6 +31,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.* @@ -50,6 +52,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat import androidx.navigation.NavController import androidx.navigation.NavHostController @@ -66,6 +69,7 @@ import com.google.firebase.ktx.Firebase import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -90,7 +94,22 @@ data class JournalEntry( val dateString: String = "" ) -data class Badge(val title: String, val description: String, val icon: ImageVector, val isUnlocked: Boolean) +data class AssessmentEntry( + val id: String = "", + val userId: String = "", + val totalScore: Int = 0, + val level: String = "", + val timestamp: com.google.firebase.Timestamp? = null, + val dateString: String = "" +) + +data class Badge( + val title: String, + val description: String, + val icon: ImageVector, + val isUnlocked: Boolean, + val dateUnlocked: String? = null +) // --- Main Activity --- class MainActivity : ComponentActivity() { @@ -161,6 +180,9 @@ fun LoginScreen(navController: NavController) { 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)) + + // Login Google dinonaktifkan sementara sesuai permintaan + /* if (isLoading) { CircularProgressIndicator() } else { @@ -170,6 +192,14 @@ fun LoginScreen(navController: NavController) { Text("Masuk dengan Google") } } + */ + Text("Opsi login sedang tidak tersedia", style = MaterialTheme.typography.bodyMedium, color = Color.Gray) + + // Tombol bypass untuk keperluan pengembangan jika diperlukan + Spacer(modifier = Modifier.height(16.dp)) + TextButton(onClick = { navController.navigate("main") { popUpTo("login") { inclusive = true } } }) { + Text("Masuk sebagai Tamu (Dev Mode)") + } } } @@ -222,23 +252,25 @@ fun AppBottomNavigation(navController: NavHostController) { } } -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() +fun calculateDailyStreak(dates: List): Int { + if (dates.isEmpty()) return 0 + val entryDates = dates.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) { 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 + if (yesterdayStr !in entryDates) return 0 } - for (i in 1 until entryDates.size) { + + while (true) { val expectedDateStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) - if (entryDates.getOrNull(i) == expectedDateStr) { + if (expectedDateStr in entryDates) { streak++ calendar.add(Calendar.DATE, -1) } else { @@ -248,16 +280,20 @@ fun calculateDailyStreak(journals: List): Int { return streak } -fun getBadges(journals: List, streak: Int): List { - val totalEntries = journals.size +fun getBadges(journals: List, assessments: List, streak: Int): List { + val totalActivities = journals.size + assessments.size val reflectionEntries = journals.count { it.type == "reflection" } - val highStressEntries = journals.count { it.mentalScore > 60 } + + // Untuk demo, kita ambil tanggal hari ini jika lencana terbuka + val today = SimpleDateFormat("d MMM yyyy", Locale("id", "ID")).format(Date()) + 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("Langkah Awal", "Lakukan aktivitas pertamamu untuk memulai perjalanan kesehatan mental.", Icons.Default.DirectionsRun, totalActivities >= 1, if(totalActivities >= 1) today else null), + Badge("Penulis Rutin", "Berhasil menulis 5 jurnal bebas sebagai bentuk ekspresi diri.", Icons.Default.EditNote, journals.size >= 5, if(journals.size >= 5) today else null), + Badge("Jiwa Reflektif", "Selesaikan 3 refleksi diri untuk mengenal dirimu lebih dalam.", Icons.Default.SelfImprovement, reflectionEntries >= 3, if(reflectionEntries >= 3) today else null), + Badge("Disiplin Diri", "Pertahankan aktivitas selama 7 hari tanpa terputus.", Icons.Default.LocalFireDepartment, streak >= 7, if(streak >= 7) today else null), + Badge("Pencari Jawaban", "Lakukan 5 penilaian harian untuk memantau kondisi mentalmu.", Icons.Default.Psychology, assessments.size >= 5, if(assessments.size >= 5) today else null), + Badge("Master Fokus", "Selesaikan total 10 aktivitas apa saja di dalam aplikasi.", Icons.Default.EmojiEvents, totalActivities >= 10, if(totalActivities >= 10) today else null) ) } @@ -268,43 +304,214 @@ fun ProfileScreen(navController: NavController) { val user = auth.currentUser val db = Firebase.firestore var journalList by remember { mutableStateOf>(emptyList()) } + var assessmentList by remember { mutableStateOf>(emptyList()) } var dailyStreak by remember { mutableIntStateOf(0) } - var badges by remember { mutableStateOf>(emptyList()) } + var selectedBadge by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + 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) + scope.launch { + val journalTask = db.collection("journals").whereEqualTo("userId", user.uid).get() + val assessmentTask = db.collection("assessments").whereEqualTo("userId", user.uid).get() + + try { + val journalSnap = journalTask.await() + val assessmentSnap = assessmentTask.await() + + journalList = journalSnap.toObjects(JournalEntry::class.java) + assessmentList = assessmentSnap.toObjects(AssessmentEntry::class.java) + + val allDates = (journalList.mapNotNull { it.timestamp?.toDate() } + + assessmentList.mapNotNull { it.timestamp?.toDate() }) + + dailyStreak = calculateDailyStreak(allDates) + } catch (e: Exception) { + Log.e("ProfileScreen", "Error loading data", e) + } } } } + + val badges = remember(journalList, assessmentList, dailyStreak) { + getBadges(journalList, assessmentList, dailyStreak) + } + + if (selectedBadge != null) { + BadgeDetailDialog(badge = selectedBadge!!, onDismiss = { selectedBadge = null }) + } + LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { - item { if (user != null) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage(model = user.photoUrl, contentDescription = "Profile Picture", modifier = Modifier.size(64.dp).clip(CircleShape)); Spacer(modifier = Modifier.width(16.dp)); Column { Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall); Text(user.email ?: "", 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 { if (user != null) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage(model = user.photoUrl, contentDescription = "Profile Picture", modifier = Modifier.size(64.dp).clip(CircleShape)); Spacer(modifier = Modifier.width(16.dp)); Column { Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall); Text(user.email ?: "Tamu", style = MaterialTheme.typography.bodyMedium) } } } else { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.AccountCircle, null, modifier = Modifier.size(64.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("Mode Tamu", style = MaterialTheme.typography.headlineSmall); Text("Masuk untuk simpan data", style = MaterialTheme.typography.bodyMedium) } } } } + + item { Card(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = Color(0xFFFFA500), modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium); Text("Streak Aktivitas Harian", style = MaterialTheme.typography.bodySmall) } } } } + item { Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth()) - LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 100.dp), contentPadding = PaddingValues(top = 8.dp), modifier = Modifier.height(120.dp).fillMaxWidth()) { - items(badges) { BadgeItem(badge = it) } + Spacer(modifier = Modifier.height(8.dp)) + } + + item { + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier.height(240.dp).fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(badges) { badge -> + BadgeItem(badge = badge, onClick = { selectedBadge = badge }) + } } } + 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) } } + Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + ReportCard("Mingguan", "Analisis 7 hari", Modifier.weight(1f)) + ReportCard("Bulanan", "Analisis 30 hari", Modifier.weight(1f)) + } + Spacer(modifier = Modifier.height(8.dp)) + ReportCard("Tahunan", "Tersedia 15 Des", Modifier.fillMaxWidth(), enabled = false) } + item { OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth()) { Text("Lihat Riwayat Jurnal Lengkap") } } } } @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 BadgeItem(badge: Badge, onClick: () -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .alpha(if (badge.isUnlocked) 1f else 0.4f) + ) { + Box( + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f)) + .border(2.dp, if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.5f), CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + badge.icon, + contentDescription = badge.title, + modifier = Modifier.size(32.dp), + tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Text( + badge.title, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + lineHeight = 14.sp + ) + } +} + +@Composable +fun BadgeDetailDialog(badge: Badge, onDismiss: () -> Unit) { + Dialog(onDismissRequest = onDismiss) { + Card( + shape = RoundedCornerShape(24.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.padding(24.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, contentDescription = "Close") } + } + + Box( + modifier = Modifier + .size(120.dp) + .clip(CircleShape) + .background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f)), + contentAlignment = Alignment.Center + ) { + Icon( + badge.icon, + contentDescription = badge.title, + modifier = Modifier.size(60.dp), + tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + if (badge.isUnlocked && badge.dateUnlocked != null) { + Surface( + color = Color(0xFF333333), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = badge.dateUnlocked, + color = Color.White, + fontSize = 12.sp, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + fontWeight = FontWeight.Bold + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } + + Text( + text = if (badge.isUnlocked) "Kamu meraih pencapaian" else "Pencapaian Terkunci", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + + Text( + text = badge.title, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + color = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = badge.description, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = onDismiss, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(50) + ) { + Text("Tutup") + } + } + } + } +} + +@Composable +fun ReportCard(title: String, subtitle: String, modifier: Modifier = Modifier, enabled: Boolean = true) { + Card( + onClick = { /* Navigasi ke laporan terkait */ }, + enabled = enabled, + modifier = modifier, + colors = if (enabled) CardDefaults.cardColors() else CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + ) { + Column(Modifier.padding(12.dp)) { + Text(title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleSmall) + Text(subtitle, style = MaterialTheme.typography.labelSmall, color = if(enabled) MaterialTheme.colorScheme.onSurfaceVariant else Color.Gray) + } } } @@ -361,7 +568,57 @@ 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().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") } } } } } +@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" } + val auth = Firebase.auth + val db = Firebase.firestore + val context = LocalContext.current + var isSaving by remember { mutableStateOf(false) } + + 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 = { + val user = auth.currentUser + if (user != null) { + isSaving = true + val entry = hashMapOf( + "userId" to user.uid, + "totalScore" to totalScore, + "level" to assessmentLevel, + "timestamp" to FieldValue.serverTimestamp(), + "dateString" to SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date()) + ) + db.collection("assessments").add(entry) + .addOnSuccessListener { + Toast.makeText(context, "Penilaian berhasil disimpan!", Toast.LENGTH_SHORT).show() + isSaving = false + } + .addOnFailureListener { + Toast.makeText(context, "Gagal menyimpan penilaian", Toast.LENGTH_SHORT).show() + isSaving = false + } + } else { + Toast.makeText(context, "Login dulu untuk menyimpan", Toast.LENGTH_SHORT).show() + } + }, + modifier = Modifier.fillMaxWidth(), + enabled = !isSaving + ) { + if (isSaving) CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White) + else Text("Selesai & Simpan") + } + } + } + } +} + @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) } } } } -- 2.47.2 From 214af2312feb4ab5cfea0563af3731966cd35cc6 Mon Sep 17 00:00:00 2001 From: Ahmar Rafly <202310715320@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 16:11:41 +0700 Subject: [PATCH 09/19] Perbaikan UI/UX --- .../com/example/ppb_kelompok2/MainActivity.kt | 440 ++++++++++++------ gradle/libs.versions.toml | 2 +- 2 files changed, 299 insertions(+), 143 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 eec4ea7..4a4a8c7 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items @@ -103,6 +104,15 @@ data class AssessmentEntry( val dateString: String = "" ) +data class CognitiveTestEntry( + val id: String = "", + val userId: String = "", + val testType: String = "", + val score: Int = 0, + val timestamp: com.google.firebase.Timestamp? = null, + val dateString: String = "" +) + data class Badge( val title: String, val description: String, @@ -280,20 +290,27 @@ fun calculateDailyStreak(dates: List): Int { return streak } -fun getBadges(journals: List, assessments: List, streak: Int): List { - val totalActivities = journals.size + assessments.size +fun getBadges(journals: List, assessments: List, cognitiveTests: List, streak: Int): List { + val totalActivities = journals.size + assessments.size + cognitiveTests.size val reflectionEntries = journals.count { it.type == "reflection" } - - // Untuk demo, kita ambil tanggal hari ini jika lencana terbuka val today = SimpleDateFormat("d MMM yyyy", Locale("id", "ID")).format(Date()) + val memoryTests = cognitiveTests.count { it.testType == "memory" } + val focusTests = cognitiveTests.count { it.testType == "focus" } + val reactionTests = cognitiveTests.count { it.testType == "reaction" } + val logicalTests = cognitiveTests.count { it.testType == "logical" } + return listOf( Badge("Langkah Awal", "Lakukan aktivitas pertamamu untuk memulai perjalanan kesehatan mental.", Icons.Default.DirectionsRun, totalActivities >= 1, if(totalActivities >= 1) today else null), Badge("Penulis Rutin", "Berhasil menulis 5 jurnal bebas sebagai bentuk ekspresi diri.", Icons.Default.EditNote, journals.size >= 5, if(journals.size >= 5) today else null), Badge("Jiwa Reflektif", "Selesaikan 3 refleksi diri untuk mengenal dirimu lebih dalam.", Icons.Default.SelfImprovement, reflectionEntries >= 3, if(reflectionEntries >= 3) today else null), Badge("Disiplin Diri", "Pertahankan aktivitas selama 7 hari tanpa terputus.", Icons.Default.LocalFireDepartment, streak >= 7, if(streak >= 7) today else null), Badge("Pencari Jawaban", "Lakukan 5 penilaian harian untuk memantau kondisi mentalmu.", Icons.Default.Psychology, assessments.size >= 5, if(assessments.size >= 5) today else null), - Badge("Master Fokus", "Selesaikan total 10 aktivitas apa saja di dalam aplikasi.", Icons.Default.EmojiEvents, totalActivities >= 10, if(totalActivities >= 10) today else null) + Badge("Master Fokus", "Selesaikan total 10 aktivitas apa saja di dalam aplikasi.", Icons.Default.EmojiEvents, totalActivities >= 10, if(totalActivities >= 10) today else null), + Badge("Ingatan Gajah", "Selesaikan 3 tes memori.", Icons.Default.Memory, memoryTests >= 3, if(memoryTests >= 3) today else null), + Badge("Mata Elang", "Selesaikan 3 tes fokus.", Icons.Default.Visibility, focusTests >= 3, if(focusTests >= 3) today else null), + Badge("Kilat Visual", "Selesaikan 3 tes kecepatan reaksi.", Icons.Default.Bolt, reactionTests >= 3, if(reactionTests >= 3) today else null), + Badge("Logika Tajam", "Selesaikan 3 tes logika.", Icons.Default.Lightbulb, logicalTests >= 3, if(logicalTests >= 3) today else null) ) } @@ -303,27 +320,34 @@ fun ProfileScreen(navController: NavController) { val auth = Firebase.auth val user = auth.currentUser val db = Firebase.firestore + val context = LocalContext.current var journalList by remember { mutableStateOf>(emptyList()) } var assessmentList by remember { mutableStateOf>(emptyList()) } + var cognitiveTestList by remember { mutableStateOf>(emptyList()) } var dailyStreak by remember { mutableIntStateOf(0) } var selectedBadge by remember { mutableStateOf(null) } + + var expanded by remember { mutableStateOf(false) } + var selectedReport by remember { mutableStateOf("Pilih Laporan") } + val reportOptions = listOf("Mingguan", "Bulanan", "Tahunan") + val scope = rememberCoroutineScope() LaunchedEffect(user) { if (user != null) { scope.launch { - val journalTask = db.collection("journals").whereEqualTo("userId", user.uid).get() - val assessmentTask = db.collection("assessments").whereEqualTo("userId", user.uid).get() - try { - val journalSnap = journalTask.await() - val assessmentSnap = assessmentTask.await() + val journalSnap = db.collection("journals").whereEqualTo("userId", user.uid).get().await() + val assessmentSnap = db.collection("assessments").whereEqualTo("userId", user.uid).get().await() + val cognitiveSnap = db.collection("cognitive_tests").whereEqualTo("userId", user.uid).get().await() journalList = journalSnap.toObjects(JournalEntry::class.java) assessmentList = assessmentSnap.toObjects(AssessmentEntry::class.java) + cognitiveTestList = cognitiveSnap.toObjects(CognitiveTestEntry::class.java) val allDates = (journalList.mapNotNull { it.timestamp?.toDate() } + - assessmentList.mapNotNull { it.timestamp?.toDate() }) + assessmentList.mapNotNull { it.timestamp?.toDate() } + + cognitiveTestList.mapNotNull { it.timestamp?.toDate() }) dailyStreak = calculateDailyStreak(allDates) } catch (e: Exception) { @@ -333,84 +357,162 @@ fun ProfileScreen(navController: NavController) { } } - val badges = remember(journalList, assessmentList, dailyStreak) { - getBadges(journalList, assessmentList, dailyStreak) + val badges = remember(journalList, assessmentList, cognitiveTestList, dailyStreak) { + getBadges(journalList, assessmentList, cognitiveTestList, dailyStreak) } - - if (selectedBadge != null) { - BadgeDetailDialog(badge = selectedBadge!!, onDismiss = { selectedBadge = null }) - } - - LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { - item { if (user != null) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage(model = user.photoUrl, contentDescription = "Profile Picture", modifier = Modifier.size(64.dp).clip(CircleShape)); Spacer(modifier = Modifier.width(16.dp)); Column { Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall); Text(user.email ?: "Tamu", style = MaterialTheme.typography.bodyMedium) } } } else { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.AccountCircle, null, modifier = Modifier.size(64.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("Mode Tamu", style = MaterialTheme.typography.headlineSmall); Text("Masuk untuk simpan data", style = MaterialTheme.typography.bodyMedium) } } } } - - item { Card(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = Color(0xFFFFA500), modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium); Text("Streak Aktivitas Harian", style = MaterialTheme.typography.bodySmall) } } } } - - item { - Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth()) - Spacer(modifier = Modifier.height(8.dp)) - } - - item { - LazyVerticalGrid( - columns = GridCells.Fixed(3), - modifier = Modifier.height(240.dp).fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(badges) { badge -> - BadgeItem(badge = badge, onClick = { selectedBadge = badge }) + + Box(modifier = Modifier.fillMaxSize()) { + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp), + contentPadding = PaddingValues(top = 16.dp, bottom = 80.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // User Info + item(span = { GridItemSpan(3) }) { + if (user != null) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) { + AsyncImage(model = user.photoUrl, contentDescription = null, modifier = Modifier.size(64.dp).clip(CircleShape)) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall) + Text(user.email ?: "Tamu", style = MaterialTheme.typography.bodyMedium) + } + } + } + } + + // Streak Card + item(span = { GridItemSpan(3) }) { + Card(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.LocalFireDepartment, null, tint = Color(0xFFFFA500), modifier = Modifier.size(40.dp)) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium) + Text("Streak Aktivitas Harian", style = MaterialTheme.typography.bodySmall) + } + } + } + } + + // Badge Header + item(span = { GridItemSpan(3) }) { + Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) + } + + // Badges Grid items + items(badges) { badge -> + BadgeItem(badge = badge) { + selectedBadge = badge + } + } + + // Report Header + item(span = { GridItemSpan(3) }) { + Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(top = 16.dp)) + } + + // Report Dropdown + item(span = { GridItemSpan(3) }) { + Box(modifier = Modifier.fillMaxWidth()) { + OutlinedCard( + modifier = Modifier.fillMaxWidth().clickable { expanded = true }, + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier.padding(16.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically) { + Text(selectedReport, fontWeight = FontWeight.SemiBold) + Icon(if (expanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, contentDescription = null) + } + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth(0.9f) + ) { + reportOptions.forEach { option -> + val description = when(option) { + "Mingguan" -> "Analisis 7 hari terakhir" + "Bulanan" -> "Analisis 30 hari terakhir" + else -> "Tersedia di akhir tahun." + } + + DropdownMenuItem( + text = { + Column { + Text(option, fontWeight = FontWeight.Bold) + Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray) + } + }, + onClick = { + if (option != "Tahunan") { + selectedReport = option + expanded = false + } else { + Toast.makeText(context, "Laporan tahunan tersedia di akhir tahun", Toast.LENGTH_SHORT).show() + } + } + ) + } + } + } + } + + // History Button + item(span = { GridItemSpan(3) }) { + OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) { + Text("Lihat Riwayat Jurnal Lengkap") } } } - - item { - Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth()) - Spacer(modifier = Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - ReportCard("Mingguan", "Analisis 7 hari", Modifier.weight(1f)) - ReportCard("Bulanan", "Analisis 30 hari", Modifier.weight(1f)) - } - Spacer(modifier = Modifier.height(8.dp)) - ReportCard("Tahunan", "Tersedia 15 Des", Modifier.fillMaxWidth(), enabled = false) + + if (selectedBadge != null) { + BadgeDetailDialog(badge = selectedBadge!!, onDismiss = { selectedBadge = null }) } - - item { OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth()) { Text("Lihat Riwayat Jurnal Lengkap") } } } } @Composable -fun BadgeItem(badge: Badge, onClick: () -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .clickable { onClick() } - .alpha(if (badge.isUnlocked) 1f else 0.4f) +fun BadgeItem(badge: Badge, modifier: Modifier = Modifier, onClick: () -> Unit) { + Surface( + onClick = onClick, + modifier = modifier.alpha(if (badge.isUnlocked) 1f else 0.4f), + color = Color.Transparent, + shape = RoundedCornerShape(12.dp) ) { - Box( - modifier = Modifier - .size(64.dp) - .clip(CircleShape) - .background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f)) - .border(2.dp, if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.5f), CircleShape), - contentAlignment = Alignment.Center + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth() ) { - Icon( - badge.icon, - contentDescription = badge.title, - modifier = Modifier.size(32.dp), - tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray + Box( + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f)) + .border(2.dp, if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.5f), CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + badge.icon, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + badge.title, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + lineHeight = 14.sp ) } - Spacer(modifier = Modifier.height(6.dp)) - Text( - badge.title, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - lineHeight = 14.sp - ) } } @@ -418,29 +520,32 @@ fun BadgeItem(badge: Badge, onClick: () -> Unit) { fun BadgeDetailDialog(badge: Badge, onDismiss: () -> Unit) { Dialog(onDismissRequest = onDismiss) { Card( - shape = RoundedCornerShape(24.dp), - modifier = Modifier.fillMaxWidth().padding(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + shape = RoundedCornerShape(28.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A)) ) { Column( modifier = Modifier.padding(24.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, contentDescription = "Close") } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, null, tint = Color.White) } + IconButton(onClick = { }) { Icon(Icons.Default.Share, null, tint = Color.White) } } + Spacer(modifier = Modifier.height(16.dp)) + Box( modifier = Modifier .size(120.dp) .clip(CircleShape) - .background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f)), + .background(if (badge.isUnlocked) MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) else Color.DarkGray), contentAlignment = Alignment.Center ) { Icon( badge.icon, - contentDescription = badge.title, - modifier = Modifier.size(60.dp), + contentDescription = null, + modifier = Modifier.size(70.dp), tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray ) } @@ -453,87 +558,56 @@ fun BadgeDetailDialog(badge: Badge, onDismiss: () -> Unit) { shape = RoundedCornerShape(8.dp) ) { Text( - text = badge.dateUnlocked, - color = Color.White, + text = badge.dateUnlocked.uppercase(), + color = Color(0xFFFFA500), fontSize = 12.sp, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - fontWeight = FontWeight.Bold + modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), + fontWeight = FontWeight.ExtraBold ) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(20.dp)) } Text( text = if (badge.isUnlocked) "Kamu meraih pencapaian" else "Pencapaian Terkunci", + color = Color.LightGray, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center ) Text( text = badge.title, + color = Color.White, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - color = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray + textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(12.dp)) Text( text = badge.description, + color = Color.Gray, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant + lineHeight = 20.sp ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(32.dp)) Button( onClick = onDismiss, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(50) + modifier = Modifier.fillMaxWidth().height(50.dp), + shape = RoundedCornerShape(25.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) ) { - Text("Tutup") + Text("Tutup", fontWeight = FontWeight.Bold) } } } } } -@Composable -fun ReportCard(title: String, subtitle: String, modifier: Modifier = Modifier, enabled: Boolean = true) { - Card( - onClick = { /* Navigasi ke laporan terkait */ }, - enabled = enabled, - modifier = modifier, - colors = if (enabled) CardDefaults.cardColors() else CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - ) { - Column(Modifier.padding(12.dp)) { - Text(title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleSmall) - Text(subtitle, style = MaterialTheme.typography.labelSmall, color = if(enabled) MaterialTheme.colorScheme.onSurfaceVariant else Color.Gray) - } - } -} - -fun scheduleReminder(context: Context, hour: Int, minute: Int) { - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val intent = Intent(context, MyReminderReceiver::class.java).apply { putExtra("title", "Waktunya Jurnaling!"); putExtra("message", "Luangkan waktu sejenak untuk refleksi diri.") } - val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - val calendar = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, hour); set(Calendar.MINUTE, minute); set(Calendar.SECOND, 0); if (before(Calendar.getInstance())) add(Calendar.DATE, 1) } - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if(alarmManager.canScheduleExactAlarms()){ - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent) - } - } else { - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent) - } - Toast.makeText(context, "Pengingat diatur!", Toast.LENGTH_SHORT).show() - } catch (e: SecurityException) { - Toast.makeText(context, "Izin alarm tidak diberikan", Toast.LENGTH_SHORT).show() - } -} - fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, indicators: Map): Int { var baseScore = 30.0 when (sentimentLabel) { "sadness", "fear", "anger" -> baseScore += (sentimentScore * 10); "joy" -> baseScore -= (sentimentScore * 10) } @@ -547,8 +621,8 @@ fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, in @OptIn(ExperimentalMaterial3Api::class) @Composable fun JournalScreen() { - var selectedTab by remember { mutableIntStateOf(0) }; var journalText by remember { mutableStateOf("") }; var reflectionAnswers by remember { mutableStateOf(List(3) { "" }) }; var isSaving by remember { mutableStateOf(false) }; var detectedEmotion by remember { mutableStateOf(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 -> + 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 depressionIndicators = listOf("suasana hati sedih", "kehilangan minat", "mudah marah", "perasaan bersalah", "perasaan tidak berharga", "sulit berkonsentrasi", "sulit mengambil keputusan", "pesimis", "menyalahkan diri sendiri", "kehilangan energi", "penurunan aktivitas", "menarik diri sosial", "gangguan tidur", "perubahan nafsu makan", "perubahan berat badan", "pikiran tentang kematian", "keinginan untuk mati", "pikiran bunuh diri", "rencana bunuh diri"); val reflectionQuestions = listOf("Satu hal kecil yang membuatmu tersenyum hari ini?", "Tantangan terbesar hari ini & solusinya?", "Satu hal yang ingin kamu perbaiki besok?") + Scaffold(topBar = { TopAppBar(title = { Text("Jurnal & Refleksi") }) }) { padding -> Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { TabRow(selectedTabIndex = selectedTab) { Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }, text = { Text("Jurnal Bebas") }); Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }, text = { Text("Refleksi") }) } Spacer(modifier = Modifier.height(16.dp)) @@ -568,7 +642,28 @@ fun JournalScreen() { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AssessmentScreen() { +fun getAssessmentColor(score: Int): Color { + return when { + score <= 4 -> Color(0xFF4CAF50) // Green (Normal) + score <= 9 -> Color(0xFFFFEB3B) // Yellow (Ringan) + score <= 14 -> Color(0xFFFF9800) // Orange (Sedang) + else -> Color(0xFFF44336) // Red (Berat) + } +} + +fun getIndicatorColor(value: Float): Color { + return when (value.toInt()) { + 0 -> Color(0xFF4CAF50) // Green + 1 -> Color(0xFFFFEB3B) // Yellow + 2 -> Color(0xFFFF9800) // Orange + 3 -> Color(0xFFF44336) // Red + else -> Color.Gray + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AssessmentScreen() { val indicators = remember { listOf("Mood Sedih", "Rasa Bersalah", "Menarik Diri", "Sulit Konsentrasi", "Lelah", "Pikiran Bunuh Diri") } val sliderValues = remember { mutableStateMapOf().apply { indicators.forEach { put(it, 0f) } } } val totalScore = sliderValues.values.sum().toInt() @@ -577,11 +672,27 @@ fun JournalScreen() { val db = Firebase.firestore val context = LocalContext.current var isSaving by remember { mutableStateOf(false) } + val summaryColor = getAssessmentColor(totalScore) Scaffold(topBar = { TopAppBar(title = { Text("Penilaian Harian") }) }) { innerPadding -> LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - item { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) { Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium); Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium); Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) } } } - items(indicators) { indicator -> IndicatorItem(indicatorName = indicator, value = sliderValues[indicator] ?: 0f) { sliderValues[indicator] = it.roundToInt().toFloat() } } + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = summaryColor.copy(alpha = 0.2f)) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium) + Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium) + Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) + } + } + } + items(indicators) { indicator -> + IndicatorItem(indicatorName = indicator, value = sliderValues[indicator] ?: 0f) { + sliderValues[indicator] = it.roundToInt().toFloat() + } + } item { Button( onClick = { @@ -609,31 +720,76 @@ fun JournalScreen() { } }, modifier = Modifier.fillMaxWidth(), - enabled = !isSaving + enabled = !isSaving, + colors = ButtonDefaults.buttonColors(containerColor = summaryColor) ) { if (isSaving) CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White) - else Text("Selesai & Simpan") + else Text("Selesai & Simpan", color = if (totalScore > 4 && totalScore <= 14) Color.Black else Color.White) } } } } } -@Composable fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) { val description = when (value.toInt()) { 0 -> "Tidak sama sekali"; 1 -> "Beberapa hari"; 2 -> "Separuh hari"; 3 -> "Hampir setiap hari"; else -> "" }; Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(indicatorName, style = MaterialTheme.typography.titleMedium); Text(text = value.toInt().toString(), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) }; Slider(value = value, onValueChange = onValueChange, valueRange = 0f..3f, steps = 2); Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally)) } } } +@Composable +fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) { + val description = when (value.toInt()) { + 0 -> "Tidak sama sekali" + 1 -> "Beberapa hari" + 2 -> "Separuh hari" + 3 -> "Hampir setiap hari" + else -> "" + } + val indicatorColor = getIndicatorColor(value) + + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text(indicatorName, style = MaterialTheme.typography.titleMedium) + Text(text = value.toInt().toString(), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) + } + Slider( + value = value, + onValueChange = onValueChange, + valueRange = 0f..3f, + steps = 2, + colors = SliderDefaults.colors( + thumbColor = indicatorColor, + activeTrackColor = indicatorColor, + inactiveTrackColor = indicatorColor.copy(alpha = 0.24f) + ) + ) + Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally)) + } + } +} + +fun saveCognitiveResult(userId: String, testType: String, score: Int) { + val db = Firebase.firestore + val entry = hashMapOf( + "userId" to userId, + "testType" to testType, + "score" to score, + "timestamp" to FieldValue.serverTimestamp(), + "dateString" to SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date()) + ) + db.collection("cognitive_tests").add(entry) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun CognitiveTestScreen(navController: NavController) { Column(modifier = Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Pilih Tes Kognitif", style = MaterialTheme.typography.headlineSmall); TestCard("Tes Memori", "Uji memori jangka pendek", Icons.Default.Memory, "memory_test", navController); TestCard("Tes Fokus", "Uji fokus & atensi", Icons.Default.CenterFocusStrong, "focus_test", navController); TestCard("Tes Kecepatan Reaksi", "Uji refleks visual", Icons.Default.Speed, "reaction_test", navController); TestCard("Tes Logika", "Uji pola pikir & logika", Icons.Default.Psychology, "logical_test", navController) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun TestCard(title: String, description: String, icon: ImageVector, route: String, navController: NavController) { Card(onClick = { navController.navigate(route) }, modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, modifier = Modifier.size(40.dp)); Spacer(Modifier.width(16.dp)); Column { Text(title, style = MaterialTheme.typography.titleMedium); Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray) } } } } data class LogicalQuestion(val question: String, val options: List, val correctAnswer: Int) -@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogicalTestScreen(navController: NavController) { val questions = remember { listOf(LogicalQuestion("Pola: 2, 4, 8, 16, ?", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Paus di air. Maka...", listOf("Paus ikan", "Paus bukan ikan", "Tak dapat disimpulkan"), 2)).shuffled() }; var currentQuestionIndex by remember { mutableIntStateOf(0) }; var score by remember { mutableIntStateOf(0) }; var isFinished by remember { mutableStateOf(false) }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Logika") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { padding -> Column(modifier = Modifier.padding(padding).padding(16.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (!isFinished) { val q = questions[currentQuestionIndex]; Text("Pertanyaan ${currentQuestionIndex + 1} / ${questions.size}"); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp)) }; Spacer(Modifier.height(24.dp)); q.options.forEachIndexed { index, option -> Button(onClick = { if (index == q.correctAnswer) score++; if (currentQuestionIndex < questions.size - 1) currentQuestionIndex++ else isFinished = true }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(option) } } } else { Text("Tes Selesai! Skor: $score/${questions.size}", style = MaterialTheme.typography.headlineMedium); Button(onClick = { navController.popBackStack() }) { Text("Kembali") } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogicalTestScreen(navController: NavController) { val questions = remember { listOf(LogicalQuestion("Pola: 2, 4, 8, 16, ?", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Paus di air. Maka...", listOf("Paus ikan", "Paus bukan ikan", "Tak dapat disimpulkan"), 2)).shuffled() }; var currentQuestionIndex by remember { mutableIntStateOf(0) }; var score by remember { mutableIntStateOf(0) }; var isFinished by remember { mutableStateOf(false) }; val auth = Firebase.auth; Scaffold(topBar = { TopAppBar(title = { Text("Tes Logika") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { padding -> Column(modifier = Modifier.padding(padding).padding(16.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (!isFinished) { val q = questions[currentQuestionIndex]; Text("Pertanyaan ${currentQuestionIndex + 1} / ${questions.size}"); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp)) }; Spacer(Modifier.height(24.dp)); q.options.forEachIndexed { index, option -> Button(onClick = { if (index == q.correctAnswer) score++; if (currentQuestionIndex < questions.size - 1) currentQuestionIndex++ else { isFinished = true; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "logical", score) } }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(option) } } } else { Text("Tes Selesai! Skor: $score/${questions.size}", style = MaterialTheme.typography.headlineMedium); Button(onClick = { navController.popBackStack() }) { Text("Kembali") } } } } } data class MemoryCard(val id: Int, val icon: ImageVector, var isFaceUp: Boolean = false, var isMatched: Boolean = false) enum class MemoryGameState { READY, PLAYING, FINISHED } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryTestScreen(navController: NavController) { val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.ThumbUp, Icons.Default.Spa, Icons.Default.Cloud, Icons.Default.Anchor); var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> MemoryCard(i, icon) }.shuffled()) }; var selectedCards by remember { mutableStateOf(listOf()) }; var moves by remember { mutableIntStateOf(0) }; var gameState by remember { mutableStateOf(MemoryGameState.READY) }; LaunchedEffect(selectedCards) { if (selectedCards.size == 2) { val (c1, c2) = selectedCards; if (c1.icon == c2.icon) { cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isMatched = true) else it } } else { delay(1000); cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isFaceUp = false) else it } }; selectedCards = emptyList() } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Memori") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Column(Modifier.padding(p).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (gameState == MemoryGameState.READY) { Button(onClick = { gameState = MemoryGameState.PLAYING }) { Text("Mulai") } } else { Text("Gerakan: $moves"); LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { card -> MemoryCardView(card) { if (!card.isFaceUp && !card.isMatched && selectedCards.size < 2) { cards = cards.map { if (it.id == card.id) it.copy(isFaceUp = true) else it }; selectedCards = selectedCards + card; if (selectedCards.size == 1) moves++ } } } }; if (cards.all { it.isMatched }) { Text("Selesai! Skor: $moves"); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0; gameState=MemoryGameState.PLAYING }) { Text("Main Lagi") } } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryTestScreen(navController: NavController) { val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.ThumbUp, Icons.Default.Spa, Icons.Default.Cloud, Icons.Default.Anchor); var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> MemoryCard(i, icon) }.shuffled()) }; var selectedCards by remember { mutableStateOf(listOf()) }; var moves by remember { mutableIntStateOf(0) }; var gameState by remember { mutableStateOf(MemoryGameState.READY) }; val auth = Firebase.auth; LaunchedEffect(selectedCards) { if (selectedCards.size == 2) { val (c1, c2) = selectedCards; if (c1.icon == c2.icon) { cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isMatched = true) else it } } else { delay(1000); cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isFaceUp = false) else it } }; selectedCards = emptyList() } }; LaunchedEffect(cards) { if (gameState == MemoryGameState.PLAYING && cards.all { it.isMatched }) { gameState = MemoryGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "memory", moves) } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Memori") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Column(Modifier.padding(p).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (gameState == MemoryGameState.READY) { Button(onClick = { gameState = MemoryGameState.PLAYING }) { Text("Mulai") } } else { Text("Gerakan: $moves"); LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { card -> MemoryCardView(card) { if (!card.isFaceUp && !card.isMatched && selectedCards.size < 2) { cards = cards.map { if (it.id == card.id) it.copy(isFaceUp = true) else it }; selectedCards = selectedCards + card; if (selectedCards.size == 1) moves++ } } } }; if (gameState == MemoryGameState.FINISHED) { Text("Selesai! Skor: $moves"); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0; gameState=MemoryGameState.PLAYING }) { Text("Main Lagi") } } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryCardView(card: MemoryCard, onClick: () -> Unit) { Card(onClick = onClick, modifier = Modifier.padding(4.dp).aspectRatio(1f)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null) } } } enum class FocusGameState { READY, PLAYING, FINISHED } data class FocusItem(val icon: ImageVector, val color: Color, val rotation: Float, val isDistractor: Boolean) -@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; LaunchedEffect(gameState) { if (gameState == FocusGameState.PLAYING) { timeLeft = selectedDuration; while (timeLeft > 0) { delay(1000); timeLeft-- }; if (timeLeft == 0) gameState = FocusGameState.FINISHED } else if (gameState == FocusGameState.FINISHED) { if (score > highScore) { highScore = score } } }; fun newLevel() { gridItems = generateFocusGrid(normalColor) }; fun restartGame() { score = 0; gameState = FocusGameState.READY; newLevel() }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { when (gameState) { FocusGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Fokus"); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score"); Text("Waktu: $timeLeft") }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, modifier = Modifier.clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!"); Text("Skor Akhir: $score"); Button(onClick = { restartGame() }) { Text("Coba Lagi") } } } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; val auth = Firebase.auth; LaunchedEffect(gameState) { if (gameState == FocusGameState.PLAYING) { timeLeft = selectedDuration; while (timeLeft > 0) { delay(1000); timeLeft-- }; if (timeLeft == 0) { gameState = FocusGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "focus", score) } } else if (gameState == FocusGameState.FINISHED) { if (score > highScore) { highScore = score } } }; fun newLevel() { gridItems = generateFocusGrid(normalColor) }; fun restartGame() { score = 0; gameState = FocusGameState.READY; newLevel() }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { when (gameState) { FocusGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Fokus"); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score"); Spacer(Modifier.width(16.dp)); Text("Waktu: $timeLeft") }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, modifier = Modifier.size(48.dp).rotate(item.rotation).clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }, tint = item.color) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!"); Text("Skor Akhir: $score"); Button(onClick = { restartGame() }) { Text("Coba Lagi") } } } } } } } private fun generateFocusGrid(normalColor: Color): List { 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 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 auth = Firebase.auth; val backgroundColor by animateColorAsState(targetValue = when (state) { ReactionGameState.WAITING -> Color.Red; ReactionGameState.ACTION -> Color.Green; else -> MaterialTheme.colorScheme.surface }, label=""); val onScreenClick: () -> Unit = { when (state) { ReactionGameState.WAITING -> { reactionTime = -1; state = ReactionGameState.FINISHED }; ReactionGameState.ACTION -> { val newTime = System.currentTimeMillis() - startTime; reactionTime = newTime; if (bestTime == null || newTime < bestTime!!) { bestTime = newTime }; state = ReactionGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "reaction", newTime.toInt()) }; else -> {} } }; LaunchedEffect(state) { if (state == ReactionGameState.WAITING) { delay(Random.nextLong(1500, 5500)); if (state == ReactionGameState.WAITING) { startTime = System.currentTimeMillis(); state = ReactionGameState.ACTION } } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Kecepatan Reaksi") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding).background(backgroundColor).clickable(enabled = state == ReactionGameState.WAITING || state == ReactionGameState.ACTION, onClick = onScreenClick), contentAlignment = Alignment.Center) { when (state) { ReactionGameState.READY -> { Column { Text("Tes Kecepatan Reaksi"); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...") }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!") }; ReactionGameState.FINISHED -> { Column { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39da456..33befc4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.1" +agp = "8.13.2" kotlin = "2.0.21" coreKtx = "1.10.1" junit = "4.13.2" -- 2.47.2 From ff5dcd893e1773159fc26cfc0aeac9ea8d3424ee Mon Sep 17 00:00:00 2001 From: HadiPrakosou-HD Date: Mon, 12 Jan 2026 16:39:22 +0700 Subject: [PATCH 10/19] 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) {} } -- 2.47.2 From 78ec7c1a89659d57a6fd9acced2a747d466edbc4 Mon Sep 17 00:00:00 2001 From: HadiPrakosou-HD Date: Mon, 12 Jan 2026 16:40:11 +0700 Subject: [PATCH 11/19] Perubahan UI UX --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd291e3..c7dc967 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -178,7 +178,7 @@ fun LoginScreen(navController: NavController) { } } */ - +// UIUX Box(modifier = Modifier.fillMaxSize().background(Brush.verticalGradient(listOf(Color.White, SoftPink)))) { Column( modifier = Modifier.fillMaxSize().padding(24.dp), -- 2.47.2 From 63d65acc354eb619f0f78d47a38d7f88b7656e3f Mon Sep 17 00:00:00 2001 From: HadiPrakosou-HD Date: Mon, 12 Jan 2026 16:41:31 +0700 Subject: [PATCH 12/19] Perubahan UI UX --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c7dc967..b431b74 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -190,7 +190,7 @@ fun LoginScreen(navController: NavController) { 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) { CircularProgressIndicator(color = HeartRed) } else { -- 2.47.2 From 23ff80ec12ddeea00e6e8e5b2f4b245849947192 Mon Sep 17 00:00:00 2001 From: nabilasuwandira <202310715066@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 16:43:42 +0700 Subject: [PATCH 13/19] fix --- .../com/example/ppb_kelompok2/MainActivity.kt | 370 +++++++++++------- .../example/ppb_kelompok2/ui/theme/Color.kt | 10 +- .../example/ppb_kelompok2/ui/theme/Theme.kt | 34 +- gradle/libs.versions.toml | 2 +- 4 files changed, 259 insertions(+), 157 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..53f47e5 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -1,10 +1,9 @@ package com.example.ppb_kelompok2 - +// Yoseph & Team - Final Version with Guest Mode (No OAuth) import android.Manifest import android.app.AlarmManager import android.app.PendingIntent import android.app.TimePickerDialog -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -20,6 +19,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -32,8 +32,10 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.* import androidx.compose.material3.* +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,6 +47,7 @@ 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 @@ -52,14 +55,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.* import coil.compose.AsyncImage import com.example.ppb_kelompok2.ui.theme.PPB_Kelompok2Theme -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.api.ApiException -import com.google.firebase.auth.ktx.auth import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.ktx.firestore import com.google.firebase.ktx.Firebase @@ -73,10 +73,10 @@ import java.util.Locale import kotlin.math.roundToInt import kotlin.random.Random -// --- Constants --- -val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" +// Kunci API sekarang diambil dari BuildConfig, bukan hardcoded +const val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" +const val GUEST_USER_ID = "guest_user_123" // ID tetap untuk mode tanpa login -// --- Data Models --- data class JournalEntry( val id: String = "", val userId: String = "", @@ -92,7 +92,6 @@ data class JournalEntry( data class Badge(val title: String, val description: String, val icon: ImageVector, val isUnlocked: Boolean) -// --- Main Activity --- class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -115,64 +114,12 @@ class MainActivity : ComponentActivity() { @Composable fun AppNavigationGraph() { val navController = rememberNavController() - val auth = Firebase.auth - val startDestination = if (auth.currentUser != null) "main" else "login" - NavHost(navController = navController, startDestination = startDestination) { - composable("login") { LoginScreen(navController = navController) } + // Langsung masuk ke "main" tanpa cek auth + NavHost(navController = navController, startDestination = "main") { composable("main") { MainAppScreen() } } } -@Composable -fun LoginScreen(navController: NavController) { - val context = LocalContext.current - var isLoading by remember { mutableStateOf(false) } - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken("532164852718-k3op2ns8b5v42k2e5qj8j1d7g2g1m3g9.apps.googleusercontent.com") - .requestEmail() - .build() - val googleSignInClient = GoogleSignIn.getClient(context, gso) - val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == ComponentActivity.RESULT_OK) { - val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) - try { - val account = task.getResult(ApiException::class.java)!! - val credential = com.google.firebase.auth.GoogleAuthProvider.getCredential(account.idToken, null) - isLoading = true - Firebase.auth.signInWithCredential(credential).addOnCompleteListener { authTask -> - isLoading = false - if (authTask.isSuccessful) { - navController.navigate("main") { popUpTo("login") { inclusive = true } } - } else { - Toast.makeText(context, "Login Gagal: ${authTask.exception?.message}", Toast.LENGTH_LONG).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() - } - } 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") - } - } - } -} - 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) @@ -202,7 +149,7 @@ fun MainAppScreen() { @Composable fun AppBottomNavigation(navController: NavHostController) { - NavigationBar { + NavigationBar(containerColor = MaterialTheme.colorScheme.surface) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route bottomNavItems.forEach { screen -> @@ -212,11 +159,16 @@ fun AppBottomNavigation(navController: NavHostController) { selected = currentRoute == screen.route, onClick = { navController.navigate(screen.route) { - popUpTo(navController.graph.startDestinationId) { saveState = true } + popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } - } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + indicatorColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f) + ) ) } } @@ -264,27 +216,38 @@ fun getBadges(journals: List, streak: Int): List { @OptIn(ExperimentalMaterial3Api::class) @Composable fun ProfileScreen(navController: NavController) { - val auth = Firebase.auth - val user = 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 -> + + LaunchedEffect(Unit) { + db.collection("journals") + .whereEqualTo("userId", GUEST_USER_ID) + .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) } } } } + item { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.AccountCircle, contentDescription = "Profile Picture", modifier = Modifier.size(64.dp), tint = MaterialTheme.colorScheme.primary) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text("Pengguna Tamu", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text("guest@mindtrack.ai", style = MaterialTheme.typography.bodyMedium) + } + } + } + item { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f))) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary); Text("Streak Jurnaling Harian", style = MaterialTheme.typography.bodySmall) } } } } item { - Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth()) + Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary) LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 100.dp), contentPadding = PaddingValues(top = 8.dp), modifier = Modifier.height(120.dp).fillMaxWidth()) { items(badges) { BadgeItem(badge = it) } } @@ -293,7 +256,7 @@ fun ProfileScreen(navController: NavController) { 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) } } + Card(onClick = { /* Navigasi ke raport */ }, enabled = isReportAvailable, modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { 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") } } } @@ -310,18 +273,12 @@ fun BadgeItem(badge: Badge) { 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 intent = Intent(context, ReminderReceiver::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() + 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() } @@ -340,73 +297,211 @@ fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, in @OptIn(ExperimentalMaterial3Api::class) @Composable fun JournalScreen() { - var selectedTab by remember { mutableIntStateOf(0) }; var journalText by remember { mutableStateOf("") }; var reflectionAnswers by remember { mutableStateOf(List(3) { "" }) }; var isSaving by remember { mutableStateOf(false) }; var detectedEmotion by remember { mutableStateOf(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 -> + 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 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", color = MaterialTheme.colorScheme.primary) }, actions = { IconButton(onClick = { timePickerDialog.show() }) { Icon(Icons.Default.Alarm, contentDescription = "Set Reminder", tint = MaterialTheme.colorScheme.primary) } }) }) { 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") }) } + TabRow(selectedTabIndex = selectedTab, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.primary, indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator(Modifier.tabIndicatorOffset(tabPositions[selectedTab]), color = MaterialTheme.colorScheme.primary) + }) { 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)) + Card(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), colors = if (isCriticalRisk) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer) else CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.2f))) { Column(modifier = Modifier.padding(16.dp)) { Text("Analisis AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary); 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.secondary); if (detectedIssues != null) Text("Indikator: $detectedIssues", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary); if (calculatedScoreFeedback != -1) Text("Skor Depresi: $calculatedScoreFeedback/100", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary); 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), colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = MaterialTheme.colorScheme.primary, cursorColor = MaterialTheme.colorScheme.primary)) } 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...") }) } } + LazyColumn(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { items(reflectionQuestions.size) { index -> Text(reflectionQuestions[index], fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary); OutlinedTextField(value = reflectionAnswers[index], onValueChange = { newValue -> val newList = reflectionAnswers.toMutableList(); newList[index] = newValue; reflectionAnswers = newList }, modifier = Modifier.fillMaxWidth(), placeholder = { Text("Jawabanmu...") }, colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = MaterialTheme.colorScheme.primary)) } } } 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 + Button(onClick = { 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") } } + val entry = hashMapOf("userId" to GUEST_USER_ID, "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(containerColor = MaterialTheme.colorScheme.primary)) { 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) @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) } } } } +@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", color = MaterialTheme.colorScheme.primary) }) }) { 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.primary)) { Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium, color = Color.White); Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium, color = Color.White); Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = Color.White) } } }; 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(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(indicatorName, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary); 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, colors = SliderDefaults.colors(thumbColor = MaterialTheme.colorScheme.primary, activeTrackColor = MaterialTheme.colorScheme.primary)); Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally), color = MaterialTheme.colorScheme.secondary) } } } +@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, color = MaterialTheme.colorScheme.primary); 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(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.4f))) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary); Spacer(Modifier.width(16.dp)); Column { Text(title, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary); Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary) } } } } 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") } } } } } +@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.AutoMirrored.Filled.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}", color = MaterialTheme.colorScheme.primary); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f))) { Text(q.question, modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.primary) }; 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, color = MaterialTheme.colorScheme.primary); 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) } } } +@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.AutoMirrored.Filled.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", color = MaterialTheme.colorScheme.primary); 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", color = MaterialTheme.colorScheme.primary); 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), colors = CardDefaults.cardColors(containerColor = if(card.isFaceUp || card.isMatched) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f))) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null, tint = Color.White) } } } enum class FocusGameState { READY, PLAYING, FINISHED } data class FocusItem(val icon: ImageVector, val color: Color, val rotation: Float, val isDistractor: Boolean) -@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; LaunchedEffect(gameState) { if (gameState == FocusGameState.PLAYING) { timeLeft = selectedDuration; while (timeLeft > 0) { delay(1000); timeLeft-- }; if (timeLeft == 0) gameState = FocusGameState.FINISHED } else if (gameState == FocusGameState.FINISHED) { if (score > highScore) { highScore = score } } }; fun newLevel() { gridItems = generateFocusGrid(normalColor) }; fun restartGame() { score = 0; gameState = FocusGameState.READY; newLevel() }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { when (gameState) { FocusGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Fokus"); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score"); Text("Waktu: $timeLeft") }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, modifier = Modifier.clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!"); Text("Skor Akhir: $score"); Button(onClick = { restartGame() }) { Text("Coba Lagi") } } } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; 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.AutoMirrored.Filled.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", color = MaterialTheme.colorScheme.primary); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score", color = MaterialTheme.colorScheme.primary); Spacer(Modifier.width(16.dp)); Text("Waktu: $timeLeft", color = MaterialTheme.colorScheme.secondary) }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, tint = item.color, modifier = Modifier.clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }.padding(8.dp).size(40.dp)) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!", color = MaterialTheme.colorScheme.primary); Text("Skor Akhir: $score", style = MaterialTheme.typography.headlineMedium); 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 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.AutoMirrored.Filled.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(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Kecepatan Reaksi", color = MaterialTheme.colorScheme.primary); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...", color = Color.White, style = MaterialTheme.typography.headlineMedium) }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!", color = Color.White, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) }; ReactionGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText, style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary); if(bestTime != null) Text("Terbaik: $bestTime ms", style = MaterialTheme.typography.bodySmall); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::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 context = LocalContext.current + val db = Firebase.firestore + var journalList by remember { mutableStateOf>(emptyList()) } + var journalToDelete by remember { mutableStateOf(null) } + var isLoading by remember { mutableStateOf(true) } + + fun fetchJournals() { + isLoading = true + db.collection("journals") + .whereEqualTo("userId", GUEST_USER_ID) + .orderBy("timestamp", com.google.firebase.firestore.Query.Direction.DESCENDING) + .get() + .addOnSuccessListener { res -> + journalList = res.documents.mapNotNull { 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") ?: "" + ) + } + isLoading = false + } + .addOnFailureListener { + Toast.makeText(context, "Gagal memuat riwayat.", Toast.LENGTH_SHORT).show() + isLoading = false + } + } + + LaunchedEffect(Unit) { + fetchJournals() + } + + journalToDelete?.let { journal -> + DeleteConfirmationDialog( + journalEntry = journal, + onConfirm = { + db.collection("journals").document(journal.id).delete() + .addOnSuccessListener { + Toast.makeText(context, "Jurnal dihapus.", Toast.LENGTH_SHORT).show() + journalList = journalList.filter { entry -> entry.id != journal.id } } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal menghapus: ${e.message}", Toast.LENGTH_SHORT).show() + } + journalToDelete = null + }, + onDismiss = { journalToDelete = null } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Riwayat Jurnal", color = MaterialTheme.colorScheme.primary) }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.primary) + } + } + ) + } + ) { paddingValues -> + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } else if (journalList.isEmpty()) { + EmptyHistoryView() + } else { + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp) + ) { + item { + Text("Semua Entri Jurnal Anda", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Spacer(modifier = Modifier.height(8.dp)) + } + if (journalList.size >= 2) { + item { + TrendGraph(journals = journalList) + Spacer(modifier = Modifier.height(8.dp)) + } + } + items(journalList, key = { it.id }) { journal -> + JournalHistoryItem(journal = journal, modifier = Modifier.animateItemPlacement(tween(300))) { + journalToDelete = journal + } + } + } + } + } +} + +@Composable +fun EmptyHistoryView() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { + Icon(Icons.Default.History, contentDescription = null, modifier = Modifier.size(80.dp), tint = MaterialTheme.colorScheme.tertiary) + Text("Belum Ada Riwayat", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text("Mulai tulis jurnal atau refleksi untuk melihat riwayatmu di sini.", textAlign = TextAlign.Center, color = Color.Gray) + } + } +} + +@Composable +fun DeleteConfirmationDialog(journalEntry: JournalEntry, onConfirm: () -> Unit, onDismiss: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Hapus Jurnal?", color = MaterialTheme.colorScheme.error) }, + text = { Text("Apakah Anda yakin ingin menghapus entri jurnal ini? Tindakan ini tidak dapat diurungkan.") }, + confirmButton = { + TextButton( + onClick = onConfirm, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) + ) { + Text("Hapus") + } + }, + dismissButton = { + TextButton(onClick = onDismiss, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary)) { + Text("Batal") + } + } + ) +} + + +@Composable +fun JournalHistoryItem(journal: JournalEntry, modifier: Modifier = Modifier, onDelete: () -> Unit) { + Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)) { + Column(modifier = Modifier.padding(16.dp)) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text(if (journal.type == "reflection") "Refleksi" else "Jurnal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) + IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) { + Icon(Icons.Default.Delete, contentDescription = "Hapus Jurnal", tint = MaterialTheme.colorScheme.error) + } + } + 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, color = MaterialTheme.colorScheme.onSurface) + if (journal.indicators.isNotEmpty()) { + Spacer(Modifier.height(12.dp)) + Text("Indikator Terdeteksi:", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary) + 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) }, colors = SuggestionChipDefaults.suggestionChipColors(labelColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f))) } } } } @@ -415,30 +510,27 @@ fun JournalHistoryScreen(navController: NavController) { @Composable fun TrendGraph(journals: List) { + val primaryColor = MaterialTheme.colorScheme.primary 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) + Text("Grafik akan muncul setelah ada minimal 2 jurnal.", textAlign = TextAlign.Center, color = Color.Gray) } } else { val dataPoints = journals.sortedBy { it.timestamp?.seconds ?: 0 }.takeLast(7).map { it.mentalScore } - Card(modifier = Modifier.fillMaxWidth().height(220.dp)) { + Card(modifier = Modifier.fillMaxWidth().height(220.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f))) { Column(modifier = Modifier.padding(16.dp)) { - Text("Grafik Skor Depresi (7 Jurnal Terakhir)") + Text("Grafik Skor Depresi (7 Jurnal Terakhir)", color = primaryColor, fontWeight = FontWeight.Bold) 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)) + drawCircle(color = primaryColor, radius = 8f, center = Offset(x, y)) } - drawPath(path, color = Color(0xFF4A90E2), style = Stroke(width = 5f)) + drawPath(path, color = primaryColor, style = Stroke(width = 5f)) } } } } } - -class MyReminderReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) {} -} diff --git a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt index 68bcd44..f7f7cb1 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt @@ -2,10 +2,18 @@ package com.example.ppb_kelompok2.ui.theme import androidx.compose.ui.graphics.Color +// Light Pink & Red Theme +val PinkLight = Color(0xFFFFD1DC) // Pink Muda +val PinkDeep = Color(0xFFFFB6C1) +val RedSoft = Color(0xFFFF5252) // Merah Lembut +val RedDeep = Color(0xFFD32F2F) // Merah Tua +val PureWhite = Color(0xFFFFFFFF) + +// Default Compose Colors (Keeping some for compatibility if needed) val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt index 386c137..c52a808 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt @@ -9,35 +9,37 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 + primary = PinkDeep, + secondary = RedSoft, + tertiary = PinkLight, + background = Color(0xFF1C1B1F), + surface = Color(0xFF1C1B1F), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.Black ) private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), + primary = RedSoft, + secondary = PinkDeep, + tertiary = PinkLight, + background = PureWhite, + surface = PureWhite, onPrimary = Color.White, onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + onTertiary = Color.Black, + surfaceVariant = PinkLight ) @Composable fun PPB_Kelompok2Theme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + dynamicColor: Boolean = false, // Turned off to force our custom theme content: @Composable () -> Unit ) { val colorScheme = when { @@ -55,4 +57,4 @@ fun PPB_Kelompok2Theme( typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39da456..33befc4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.1" +agp = "8.13.2" kotlin = "2.0.21" coreKtx = "1.10.1" junit = "4.13.2" -- 2.47.2 From 8253b4adebf2f33c57c5338b380da8381ea80c26 Mon Sep 17 00:00:00 2001 From: nabilasuwandira <202310715066@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 17:15:00 +0700 Subject: [PATCH 14/19] fix --- .../com/example/ppb_kelompok2/MainActivity.kt | 82 ++++++++++++------- .../example/ppb_kelompok2/ui/theme/Color.kt | 15 ++-- .../example/ppb_kelompok2/ui/theme/Theme.kt | 26 +++--- 3 files changed, 72 insertions(+), 51 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 53f47e5..890d00f 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -1,5 +1,5 @@ package com.example.ppb_kelompok2 -// Yoseph & Team - Final Version with Guest Mode (No OAuth) +// Yoseph & Team - Final Version with Gradient Background import android.Manifest import android.app.AlarmManager import android.app.PendingIntent @@ -43,6 +43,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate 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 @@ -75,7 +76,7 @@ import kotlin.random.Random // Kunci API sekarang diambil dari BuildConfig, bukan hardcoded const val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" -const val GUEST_USER_ID = "guest_user_123" // ID tetap untuk mode tanpa login +const val GUEST_USER_ID = "guest_user_123" data class JournalEntry( val id: String = "", @@ -92,14 +93,30 @@ data class JournalEntry( data class Badge(val title: String, val description: String, val icon: ImageVector, val isUnlocked: Boolean) +@Composable +fun AppGradientBackground(content: @Composable () -> Unit) { + val gradient = Brush.verticalGradient( + colors = listOf( + Color(0xFFFFD1DC), // Pink Muda + Color(0xFFFF5252).copy(alpha = 0.3f), // Merah Lembut Transparan + Color.White // Putih + ) + ) + Box(modifier = Modifier.fillMaxSize().background(gradient)) { + content() + } +} + class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { PPB_Kelompok2Theme { - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { - AppNavigationGraph() + AppGradientBackground { + Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { + AppNavigationGraph() + } } } } @@ -114,7 +131,6 @@ class MainActivity : ComponentActivity() { @Composable fun AppNavigationGraph() { val navController = rememberNavController() - // Langsung masuk ke "main" tanpa cek auth NavHost(navController = navController, startDestination = "main") { composable("main") { MainAppScreen() } } @@ -132,7 +148,10 @@ val bottomNavItems = listOf(Screen.Journal, Screen.Assessment, Screen.CognitiveT @Composable fun MainAppScreen() { val navController = rememberNavController() - Scaffold(bottomBar = { AppBottomNavigation(navController = navController) }) { innerPadding -> + Scaffold( + bottomBar = { AppBottomNavigation(navController = navController) }, + containerColor = Color.Transparent // Penting agar gradient di bawah terlihat + ) { innerPadding -> NavHost(navController = navController, startDestination = Screen.Journal.route, modifier = Modifier.padding(innerPadding)) { composable(Screen.Journal.route) { JournalScreen() } composable(Screen.Assessment.route) { AssessmentScreen() } @@ -149,7 +168,7 @@ fun MainAppScreen() { @Composable fun AppBottomNavigation(navController: NavHostController) { - NavigationBar(containerColor = MaterialTheme.colorScheme.surface) { + NavigationBar(containerColor = Color.White.copy(alpha = 0.8f)) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route bottomNavItems.forEach { screen -> @@ -245,7 +264,7 @@ fun ProfileScreen(navController: NavController) { } } } - item { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f))) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary); Text("Streak Jurnaling Harian", style = MaterialTheme.typography.bodySmall) } } } } + item { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.7f))) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary); Text("Streak Jurnaling Harian", style = MaterialTheme.typography.bodySmall) } } } } item { Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary) LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 100.dp), contentPadding = PaddingValues(top = 8.dp), modifier = Modifier.height(120.dp).fillMaxWidth()) { @@ -256,9 +275,9 @@ fun ProfileScreen(navController: NavController) { 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(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { 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) } } + Card(onClick = { /* Navigasi ke raport */ }, enabled = isReportAvailable, modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.5f))) { 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 { OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.primary)) { Text("Lihat Riwayat Jurnal Lengkap") } } } } @@ -298,17 +317,20 @@ fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, in @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 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", color = MaterialTheme.colorScheme.primary) }, actions = { IconButton(onClick = { timePickerDialog.show() }) { Icon(Icons.Default.Alarm, contentDescription = "Set Reminder", tint = MaterialTheme.colorScheme.primary) } }) }) { padding -> + Scaffold( + topBar = { TopAppBar(title = { Text("Jurnal & Refleksi", color = MaterialTheme.colorScheme.primary) }, actions = { IconButton(onClick = { timePickerDialog.show() }) { Icon(Icons.Default.Alarm, contentDescription = "Set Reminder", tint = MaterialTheme.colorScheme.primary) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)) }, + containerColor = Color.Transparent + ) { padding -> Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - TabRow(selectedTabIndex = selectedTab, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.primary, indicator = { tabPositions -> + TabRow(selectedTabIndex = selectedTab, containerColor = Color.White.copy(alpha = 0.5f), contentColor = MaterialTheme.colorScheme.primary, indicator = { tabPositions -> TabRowDefaults.SecondaryIndicator(Modifier.tabIndicatorOffset(tabPositions[selectedTab]), color = MaterialTheme.colorScheme.primary) }) { 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(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.2f))) { Column(modifier = Modifier.padding(16.dp)) { Text("Analisis AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary); 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.secondary); if (detectedIssues != null) Text("Indikator: $detectedIssues", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary); if (calculatedScoreFeedback != -1) Text("Skor Depresi: $calculatedScoreFeedback/100", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary); 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), colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = MaterialTheme.colorScheme.primary, cursorColor = MaterialTheme.colorScheme.primary)) + Card(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), colors = if (isCriticalRisk) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer) else CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.8f))) { Column(modifier = Modifier.padding(16.dp)) { Text("Analisis AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary); 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.secondary); if (detectedIssues != null) Text("Indikator: $detectedIssues", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary); if (calculatedScoreFeedback != -1) Text("Skor Depresi: $calculatedScoreFeedback/100", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary); 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), colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = MaterialTheme.colorScheme.primary, cursorColor = MaterialTheme.colorScheme.primary, focusedContainerColor = Color.White.copy(alpha = 0.6f), unfocusedContainerColor = Color.White.copy(alpha = 0.6f))) } else { - LazyColumn(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { items(reflectionQuestions.size) { index -> Text(reflectionQuestions[index], fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary); OutlinedTextField(value = reflectionAnswers[index], onValueChange = { newValue -> val newList = reflectionAnswers.toMutableList(); newList[index] = newValue; reflectionAnswers = newList }, modifier = Modifier.fillMaxWidth(), placeholder = { Text("Jawabanmu...") }, colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = MaterialTheme.colorScheme.primary)) } } + LazyColumn(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { items(reflectionQuestions.size) { index -> Text(reflectionQuestions[index], fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary); OutlinedTextField(value = reflectionAnswers[index], onValueChange = { newValue -> val newList = reflectionAnswers.toMutableList(); newList[index] = newValue; reflectionAnswers = newList }, modifier = Modifier.fillMaxWidth(), placeholder = { Text("Jawabanmu...") }, colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = MaterialTheme.colorScheme.primary, focusedContainerColor = Color.White.copy(alpha = 0.6f), unfocusedContainerColor = Color.White.copy(alpha = 0.6f))) } } } Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { 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 @@ -320,26 +342,26 @@ 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().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", color = MaterialTheme.colorScheme.primary) }) }) { 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.primary)) { Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium, color = Color.White); Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium, color = Color.White); Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = Color.White) } } }; 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(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(indicatorName, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary); 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, colors = SliderDefaults.colors(thumbColor = MaterialTheme.colorScheme.primary, activeTrackColor = MaterialTheme.colorScheme.primary)); Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally), color = MaterialTheme.colorScheme.secondary) } } } -@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, color = MaterialTheme.colorScheme.primary); 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(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.4f))) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary); Spacer(Modifier.width(16.dp)); Column { Text(title, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary); Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary) } } } } +@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", color = MaterialTheme.colorScheme.primary) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)) }, containerColor = Color.Transparent) { 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.primary)) { Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium, color = Color.White); Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium, color = Color.White); Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = Color.White) } } }; 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(), colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.6f))) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(indicatorName, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary); 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, colors = SliderDefaults.colors(thumbColor = MaterialTheme.colorScheme.primary, activeTrackColor = MaterialTheme.colorScheme.primary)); Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally), color = MaterialTheme.colorScheme.secondary) } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CognitiveTestScreen(navController: NavController) { Box(Modifier.fillMaxSize()) { 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, color = MaterialTheme.colorScheme.primary); 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(), colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.7f))) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary); Spacer(Modifier.width(16.dp)); Column { Text(title, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary); Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary) } } } } 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.AutoMirrored.Filled.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}", color = MaterialTheme.colorScheme.primary); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f))) { Text(q.question, modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.primary) }; 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, color = MaterialTheme.colorScheme.primary); Button(onClick = { navController.popBackStack() }) { Text("Kembali") } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogicalTestScreen(navController: NavController) { val questions = remember { listOf(LogicalQuestion("Pola: 2, 4, 8, 16, ?", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Paus di air. Maka...", listOf("Paus ikan", "Paus bukan ikan", "Tak dapat disimpulkan"), 2)).shuffled() }; var currentQuestionIndex by remember { mutableIntStateOf(0) }; var score by remember { mutableIntStateOf(0) }; var isFinished by remember { mutableStateOf(false) }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Logika") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)) }, containerColor = Color.Transparent) { 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}", color = MaterialTheme.colorScheme.primary); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.8f))) { Text(q.question, modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.primary) }; 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, color = MaterialTheme.colorScheme.primary); 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.AutoMirrored.Filled.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", color = MaterialTheme.colorScheme.primary); 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", color = MaterialTheme.colorScheme.primary); 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), colors = CardDefaults.cardColors(containerColor = if(card.isFaceUp || card.isMatched) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f))) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null, tint = Color.White) } } } +@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.AutoMirrored.Filled.ArrowBack,null)}}, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent))}, containerColor = Color.Transparent) { 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", color = MaterialTheme.colorScheme.primary); 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", color = MaterialTheme.colorScheme.primary); 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), colors = CardDefaults.cardColors(containerColor = if(card.isFaceUp || card.isMatched) MaterialTheme.colorScheme.primary else Color.White.copy(alpha = 0.6f))) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null, tint = Color.White) } } } 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.AutoMirrored.Filled.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", color = MaterialTheme.colorScheme.primary); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score", color = MaterialTheme.colorScheme.primary); Spacer(Modifier.width(16.dp)); Text("Waktu: $timeLeft", color = MaterialTheme.colorScheme.secondary) }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, tint = item.color, modifier = Modifier.clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }.padding(8.dp).size(40.dp)) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!", color = MaterialTheme.colorScheme.primary); Text("Skor Akhir: $score", style = MaterialTheme.typography.headlineMedium); Button(onClick = { restartGame() }) { Text("Coba Lagi") } } } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; 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.AutoMirrored.Filled.ArrowBack, null) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)) }, containerColor = Color.Transparent) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { when (gameState) { FocusGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Fokus", color = MaterialTheme.colorScheme.primary); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score", color = MaterialTheme.colorScheme.primary); Spacer(Modifier.width(16.dp)); Text("Waktu: $timeLeft", color = MaterialTheme.colorScheme.secondary) }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, tint = item.color, modifier = Modifier.clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }.padding(8.dp).size(40.dp)) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!", color = MaterialTheme.colorScheme.primary); Text("Skor Akhir: $score", style = MaterialTheme.typography.headlineMedium); 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.AutoMirrored.Filled.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(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Kecepatan Reaksi", color = MaterialTheme.colorScheme.primary); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...", color = Color.White, style = MaterialTheme.typography.headlineMedium) }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!", color = Color.White, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) }; ReactionGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText, style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary); if(bestTime != null) Text("Terbaik: $bestTime ms", style = MaterialTheme.typography.bodySmall); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReactionSpeedTestScreen(navController: NavController) { var state by remember { mutableStateOf(ReactionGameState.READY) }; var startTime by remember { mutableLongStateOf(0L) }; var reactionTime by remember { mutableLongStateOf(0L) }; var bestTime by remember { mutableStateOf(null) }; val backgroundColor by animateColorAsState(targetValue = when (state) { ReactionGameState.WAITING -> Color.Red; ReactionGameState.ACTION -> Color.Green; else -> Color.Transparent }, 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.AutoMirrored.Filled.ArrowBack, null) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)) }, containerColor = backgroundColor) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding).clickable(enabled = state == ReactionGameState.WAITING || state == ReactionGameState.ACTION, onClick = onScreenClick), contentAlignment = Alignment.Center) { when (state) { ReactionGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Kecepatan Reaksi", color = MaterialTheme.colorScheme.primary); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...", color = Color.White, style = MaterialTheme.typography.headlineMedium) }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!", color = Color.White, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) }; ReactionGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText, style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary); if(bestTime != null) Text("Terbaik: $bestTime ms", style = MaterialTheme.typography.bodySmall); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable -fun JournalHistoryScreen(navController: NavController) { +fun JournalHistoryScreen(navController: NavHostController) { val context = LocalContext.current val db = Firebase.firestore var journalList by remember { mutableStateOf>(emptyList()) } @@ -404,9 +426,11 @@ fun JournalHistoryScreen(navController: NavController) { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.primary) } - } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) ) - } + }, + containerColor = Color.Transparent ) { paddingValues -> if (isLoading) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -484,7 +508,7 @@ fun DeleteConfirmationDialog(journalEntry: JournalEntry, onConfirm: () -> Unit, @Composable fun JournalHistoryItem(journal: JournalEntry, modifier: Modifier = Modifier, onDelete: () -> Unit) { - Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)) { + Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.8f)), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)) { Column(modifier = Modifier.padding(16.dp)) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Text(if (journal.type == "reflection") "Refleksi" else "Jurnal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) @@ -517,7 +541,7 @@ fun TrendGraph(journals: List) { } } else { val dataPoints = journals.sortedBy { it.timestamp?.seconds ?: 0 }.takeLast(7).map { it.mentalScore } - Card(modifier = Modifier.fillMaxWidth().height(220.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f))) { + Card(modifier = Modifier.fillMaxWidth().height(220.dp), colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.6f))) { Column(modifier = Modifier.padding(16.dp)) { Text("Grafik Skor Depresi (7 Jurnal Terakhir)", color = primaryColor, fontWeight = FontWeight.Bold) Canvas(modifier = Modifier.fillMaxWidth().height(150.dp)) { diff --git a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt index f7f7cb1..a3bdc16 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt @@ -2,18 +2,17 @@ package com.example.ppb_kelompok2.ui.theme import androidx.compose.ui.graphics.Color -// Light Pink & Red Theme -val PinkLight = Color(0xFFFFD1DC) // Pink Muda -val PinkDeep = Color(0xFFFFB6C1) -val RedSoft = Color(0xFFFF5252) // Merah Lembut -val RedDeep = Color(0xFFD32F2F) // Merah Tua -val PureWhite = Color(0xFFFFFFFF) +// 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 -// Default Compose Colors (Keeping some for compatibility if needed) +// Tetap pertahankan default jika diperlukan (opsional) val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) val Pink80 = Color(0xFFEFB8C8) - val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt index c52a808..ff9cfdf 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt @@ -13,9 +13,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( - primary = PinkDeep, - secondary = RedSoft, - tertiary = PinkLight, + primary = MerahUtama, + secondary = PinkAksen, + tertiary = PinkMuda, background = Color(0xFF1C1B1F), surface = Color(0xFF1C1B1F), onPrimary = Color.White, @@ -24,22 +24,21 @@ private val DarkColorScheme = darkColorScheme( ) private val LightColorScheme = lightColorScheme( - primary = RedSoft, - secondary = PinkDeep, - tertiary = PinkLight, - background = PureWhite, - surface = PureWhite, - onPrimary = Color.White, - onSecondary = Color.White, + primary = MerahUtama, + secondary = MerahTua, + tertiary = PinkMuda, + background = PutihMurni, + surface = PutihMurni, + onPrimary = PutihMurni, + onSecondary = PutihMurni, onTertiary = Color.Black, - surfaceVariant = PinkLight + surfaceVariant = PinkMuda ) @Composable fun PPB_Kelompok2Theme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = false, // Turned off to force our custom theme + dynamicColor: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { @@ -47,7 +46,6 @@ fun PPB_Kelompok2Theme( val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme else -> LightColorScheme } -- 2.47.2 From 85345c4052296fc7bad7bd49ab128fae9a85a95d Mon Sep 17 00:00:00 2001 From: nabilasuwandira <202310715066@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 17:25:22 +0700 Subject: [PATCH 15/19] fix --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 890d00f..a5ee452 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -466,7 +466,7 @@ fun JournalHistoryScreen(navController: NavHostController) { } } } - +//ui @Composable fun EmptyHistoryView() { Box( -- 2.47.2 From 8c765fc7e2a13d140eb5e499618387f72a3fc6fd Mon Sep 17 00:00:00 2001 From: nabilasuwandira <202310715066@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 17:28:56 +0700 Subject: [PATCH 16/19] fix --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a5ee452..4a42d69 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -466,7 +466,7 @@ fun JournalHistoryScreen(navController: NavHostController) { } } } -//ui +//ui ux @Composable fun EmptyHistoryView() { Box( -- 2.47.2 From 27ff57548f49608bcee3fde17b81d8c73c498945 Mon Sep 17 00:00:00 2001 From: nabilasuwandira <202310715066@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 17:29:32 +0700 Subject: [PATCH 17/19] fix --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..8ed8e4e 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -412,7 +412,7 @@ fun JournalHistoryScreen(navController: NavController) { } } } - +//eee @Composable fun TrendGraph(journals: List) { if (journals.size < 2) { -- 2.47.2 From be5e723bca6207571c8a26f1fb3932a271b5110a Mon Sep 17 00:00:00 2001 From: nabilasuwandira <202310715066@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 17:29:44 +0700 Subject: [PATCH 18/19] fix --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ed8e4e..97ec644 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -412,7 +412,7 @@ fun JournalHistoryScreen(navController: NavController) { } } } -//eee +//eeeuiux @Composable fun TrendGraph(journals: List) { if (journals.size < 2) { -- 2.47.2 From 2cc0e9405708401d3e1b0082e77b29ca3c460df6 Mon Sep 17 00:00:00 2001 From: nabilasuwandira <202310715066@mhs.ubharajaya.ac.id> Date: Mon, 12 Jan 2026 17:30:25 +0700 Subject: [PATCH 19/19] fix --- app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 97ec644..ca5b0e4 100644 --- a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -412,7 +412,7 @@ fun JournalHistoryScreen(navController: NavController) { } } } -//eeeuiux +//eeeuiuxuuiui @Composable fun TrendGraph(journals: List) { if (journals.size < 2) { -- 2.47.2