869 lines
33 KiB
Kotlin
869 lines
33 KiB
Kotlin
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<String, Float>().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<MemoryCard>()) }
|
|
var moves by remember { mutableIntStateOf(0) }
|
|
var bestScore by remember { mutableStateOf<Int?>(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<ImageVector>): List<MemoryCard> {
|
|
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<FocusItem> {
|
|
val gridSize = 25
|
|
val normalIcon = Icons.Default.Circle
|
|
val distractorIndex = Random.nextInt(gridSize)
|
|
|
|
val distractorType = Random.nextInt(3)
|
|
val distractor: FocusItem
|
|
val items = MutableList(gridSize) { FocusItem(normalIcon, normalColor, 0f, false) }
|
|
|
|
when (distractorType) {
|
|
0 -> { // 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<Long?>(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)
|
|
}
|
|
}
|
|
}
|
|
}
|