commit c09fb130fc48ed5e59cac7c61fa3a2da13298d70 Author: Ahmar Rafly <202310715320@mhs.ubharajaya.ac.id> Date: Thu Dec 11 13:57:27 2025 +0700 Tampilan UI/UX dari Project Jurnaling Psikologi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..c3f21c8 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.example.ppb_kelompok2" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "com.example.ppb_kelompok2" + minSdk = 25 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.navigation:navigation-compose:2.9.6") + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/ppb_kelompok2/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/ppb_kelompok2/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ae0dd4f --- /dev/null +++ b/app/src/androidTest/java/com/example/ppb_kelompok2/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.ppb_kelompok2 + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.ppb_kelompok2", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2c56ca6 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt new file mode 100644 index 0000000..076094f --- /dev/null +++ b/app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt @@ -0,0 +1,868 @@ +package com.example.ppb_kelompok2 +// Yosep +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +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.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +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.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.* +import com.example.ppb_kelompok2.ui.theme.PPB_Kelompok2Theme +import kotlinx.coroutines.delay +import kotlin.math.roundToInt +import kotlin.random.Random + +// --- Main Activity --- +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PPB_Kelompok2Theme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + AppNavigationGraph() + } + } + } + } +} + +// --- Navigation Graph --- +@Composable +fun AppNavigationGraph() { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = "login") { + 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 + ) { + 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) + ) + 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") + } + } +} + +// --- 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) +} + +val bottomNavItems = listOf( + Screen.Journal, + Screen.Assessment, + Screen.CognitiveTest, + Screen.History +) + +@Composable +fun MainAppScreen() { + val navController = rememberNavController() + Scaffold( + bottomBar = { AppBottomNavigation(navController = navController) } + ) { innerPadding -> + AppNavHost(navController = navController, modifier = Modifier.padding(innerPadding)) + } +} + +@Composable +fun AppBottomNavigation(navController: NavHostController) { + NavigationBar { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + bottomNavItems.forEach { screen -> + NavigationBarItem( + icon = { Icon(screen.icon, contentDescription = screen.label) }, + label = { Text(screen.label) }, + selected = currentRoute == screen.route, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true + } + } + ) + } + } +} + +@Composable +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) } + } +} + +// --- App Screens --- + +@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 } + } 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)) + } + } + } +} + +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") + } + } + } + } + } + } +} + +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") + } + } + } + } + } + } +} + + +@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) + } + } + } +} 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 new file mode 100644 index 0000000..68bcd44 --- /dev/null +++ b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.ppb_kelompok2.ui.theme + +import androidx.compose.ui.graphics.Color + +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 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 new file mode 100644 index 0000000..386c137 --- /dev/null +++ b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.example.ppb_kelompok2.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +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), + */ +) + +@Composable +fun PPB_Kelompok2Theme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Type.kt b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Type.kt new file mode 100644 index 0000000..b90d30e --- /dev/null +++ b/app/src/main/java/com/example/ppb_kelompok2/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.ppb_kelompok2.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3cc4c5b --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + PPB_Kelompok2 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..85fd58f --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +