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..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" }