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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ 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..0d13ffa 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,23 +1,36 @@
+import java.util.Properties
+import java.io.FileInputStream
+
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
+ 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 {
- 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") ?: ""}\"")
}
buildTypes {
@@ -38,6 +51,7 @@ android {
}
buildFeatures {
compose = true
+ buildConfig = true // Ensure this is enabled
}
}
@@ -52,6 +66,15 @@ 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)
+ implementation(libs.coil.compose)
+
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..c189657
--- /dev/null
+++ b/app/src/main/java/com/example/ppb_kelompok2/HuggingFaceApi.kt
@@ -0,0 +1,79 @@
+package com.example.ppb_kelompok2
+
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+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 ---
+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 (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 d4c5490..ca5b0e4 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
+
+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
+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,27 +28,70 @@ 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.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
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.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 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
+import kotlinx.coroutines.async
import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+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 = "",
+ val content: String = "",
+ val type: String = "journal",
+ val sentiment: String = "",
+ val confidence: Float = 0f,
+ val indicators: Map = emptyMap(),
+ val mentalScore: Int = 0,
+ 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)
+
// --- Main Activity ---
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -47,76 +104,99 @@ 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)
+ }
+ }
}
}
-// --- Navigation Graph ---
@Composable
fun AppNavigationGraph() {
val navController = rememberNavController()
- NavHost(navController = navController, startDestination = "login") {
+ val auth = Firebase.auth
+ val startDestination = if (auth.currentUser != null) "main" else "login"
+ NavHost(navController = navController, startDestination = startDestination) {
composable("login") { LoginScreen(navController = navController) }
composable("main") { MainAppScreen() }
}
}
-
-// --- Login Screen ---
@Composable
fun LoginScreen(navController: NavController) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
+ 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 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")
+ }
}
}
}
-// --- Main App Structure (with Bottom Navigation) ---
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 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() {
val navController = rememberNavController()
- Scaffold(
- bottomBar = { AppBottomNavigation(navController = navController) }
- ) { innerPadding ->
- AppNavHost(navController = navController, modifier = Modifier.padding(innerPadding))
+ 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) }
+ }
}
}
@@ -125,7 +205,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) },
@@ -143,726 +222,223 @@ 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) }
+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") } }
}
}
-// --- App Screens ---
+@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)
+ }
+ 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 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")
- }
- }
- 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")
- }
- }
-}
-
-@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 { 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"
- }
-
- 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),
- horizontalAlignment = Alignment.Start
- ) {
- 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")
- }
- }
- }
- }
-}
-
-@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
- ) {
- Text(indicatorName, style = MaterialTheme.typography.titleMedium)
- 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)
- )
- }
- }
-}
-
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun CognitiveTestScreen(navController: NavController) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
- 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"
- )
- }
-}
-
-// --- 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)) }
- 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 }
+ 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 {
- 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")
- }
- }
- }
- }
- }
- }
- }
-}
-
-fun createShuffledCards(icons: List): List {
- return (icons + icons).mapIndexed { index, icon -> MemoryCard(id = index, icon = icon) }.shuffled()
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@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))
+ 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") } }
}
}
}
+@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 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)
+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)
}
- 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")
- }
+ 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) }) }
}
}
- 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")
- }
}
}
}
}
}
}
-
-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)
+//eeeuiuxuuiui
@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 */ }
+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)
}
- }
-
- 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")
- }
+ } 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))
}
}
}
}
}
-
-@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 {
- 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)
- }
- }
- }
- }
-}
-
-@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)
- }
- }
- }
+class MyReminderReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {}
}
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/java/com/example/ppb_kelompok2/ui/theme/Color.kt b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt
index 68bcd44..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,10 +2,17 @@ package com.example.ppb_kelompok2.ui.theme
import androidx.compose.ui.graphics.Color
+// Pink Muda, Merah, dan Putih Theme
+val PinkMuda = Color(0xFFFFD1DC) // Pink sangat muda
+val MerahUtama = Color(0xFFFF5252) // Merah cerah
+val MerahTua = Color(0xFFD32F2F) // Merah gelap untuk teks/aksen
+val PutihMurni = Color(0xFFFFFFFF) // Putih
+val PinkAksen = Color(0xFFFFB6C1) // Pink untuk variasi
+
+// Tetap pertahankan default jika diperlukan (opsional)
val Purple80 = Color(0xFFD0BCFF)
val 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..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
@@ -9,35 +9,36 @@ 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 = MerahUtama,
+ secondary = PinkAksen,
+ tertiary = PinkMuda,
+ 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),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
+ primary = MerahUtama,
+ secondary = MerahTua,
+ tertiary = PinkMuda,
+ background = PutihMurni,
+ surface = PutihMurni,
+ onPrimary = PutihMurni,
+ onSecondary = PutihMurni,
+ onTertiary = Color.Black,
+ surfaceVariant = PinkMuda
)
@Composable
fun PPB_Kelompok2Theme(
darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
+ dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
@@ -45,7 +46,6 @@ fun PPB_Kelompok2Theme(
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
-
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
@@ -55,4 +55,4 @@ fun PPB_Kelompok2Theme(
typography = Typography,
content = content
)
-}
\ 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..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"
@@ -8,6 +8,12 @@ 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"
+coil = "2.5.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,9 +30,17 @@ 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" }
+coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
[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" }