diff --git a/core/designsystem/src/main/kotlin/dev/adriankuta/kahootquiz/core/designsystem/Color.kt b/core/designsystem/src/main/kotlin/dev/adriankuta/kahootquiz/core/designsystem/Color.kt index 0242dce..0d96ca4 100644 --- a/core/designsystem/src/main/kotlin/dev/adriankuta/kahootquiz/core/designsystem/Color.kt +++ b/core/designsystem/src/main/kotlin/dev/adriankuta/kahootquiz/core/designsystem/Color.kt @@ -19,11 +19,12 @@ val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) -val Grey = Color(0xFFFAFAFA) -val Pink = Color(0xFFFF99AA) -val Red = Color(0xFFFF3355) -val Red2 = Color(0xFFE21B3C) val Blue2 = Color(0xFF1368CE) -val Yellow3 = Color(0xFFD89E00) val Green = Color(0xFF66BF39) val Green2 = Color(0xFF26890C) +val Grey = Color(0xFFFAFAFA) +val Pink = Color(0xFFFF99AA) +val Purple = Color(0xFF864CBF) +val Red = Color(0xFFFF3355) +val Red2 = Color(0xFFE21B3C) +val Yellow3 = Color(0xFFD89E00) diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreen.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreen.kt index eecec68..53796ad 100644 --- a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreen.kt +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreen.kt @@ -1,5 +1,8 @@ package dev.adriankuta.kahootquiz.ui.quiz +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -26,6 +29,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -43,6 +47,7 @@ import dev.adriankuta.kahootquiz.core.designsystem.Green2 import dev.adriankuta.kahootquiz.core.designsystem.Grey import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme import dev.adriankuta.kahootquiz.core.designsystem.Pink +import dev.adriankuta.kahootquiz.core.designsystem.Purple import dev.adriankuta.kahootquiz.core.designsystem.Red import dev.adriankuta.kahootquiz.core.designsystem.Red2 import dev.adriankuta.kahootquiz.core.designsystem.Yellow3 @@ -90,6 +95,8 @@ private fun QuizScreen( .fillMaxWidth() .height(72.dp) .padding(8.dp), + currentQuestionIndex = uiState.currentQuestionIndex, + totalQuestions = uiState.totalQuestions, ) QuestionContent( question = uiState.currentQuestion ?: return, @@ -101,6 +108,12 @@ private fun QuizScreen( answer = uiState.answer, onSelect = onSelect, ) + // Timer below choices + TimerBar( + totalSeconds = uiState.totalTimeSeconds, + remainingSeconds = uiState.remainingTimeSeconds, + modifier = Modifier.padding(8.dp), + ) } } } @@ -108,17 +121,19 @@ private fun QuizScreen( @Composable private fun Toolbar( modifier: Modifier = Modifier, + currentQuestionIndex: Int = 0, + totalQuestions: Int = 0, ) { Box( modifier = modifier, ) { Text( - text = "2/24", + text = "${currentQuestionIndex + 1}/$totalQuestions", modifier = Modifier .align(Alignment.CenterStart) .background( color = Grey, - shape = RoundedCornerShape(60.dp), + shape = RoundedCornerShape(percent = 50), ) .padding(horizontal = 8.dp, vertical = 4.dp), ) @@ -128,7 +143,7 @@ private fun Toolbar( .align(Alignment.Center) .background( color = Grey, - shape = RoundedCornerShape(60.dp), + shape = RoundedCornerShape(percent = 50), ) .padding(horizontal = 8.dp, vertical = 4.dp), ) { @@ -319,6 +334,37 @@ private fun ChoiceItemRevealed( } } +@Composable +private fun TimerBar( + totalSeconds: Int, + remainingSeconds: Int, + modifier: Modifier = Modifier, +) { + + val progress: Float by animateFloatAsState( + targetValue = (remainingSeconds.toFloat()) / totalSeconds, + label = "Timer", + animationSpec = tween(durationMillis = 1000, easing = LinearEasing), + ) + + Box( + modifier = modifier + .fillMaxWidth(progress) + .background( + color = Purple, + shape = RoundedCornerShape(percent = 50), + ), + ) { + Text( + text = "$remainingSeconds", + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 8.dp) + .clipToBounds(), + color = Color.White, + ) + } +} @Preview @Composable @@ -342,6 +388,9 @@ private fun QuizScreenPreview() { QuizScreen( uiState = QuizUiState( currentQuestion = sampleQuestion, + totalQuestions = 12, + totalTimeSeconds = 30, + remainingTimeSeconds = 10, ), onSelect = {}, ) @@ -373,6 +422,9 @@ private fun QuizScreenRevealedAnswerPreview() { answer = AnswerUiState( selectedChoiceIndex = 1, ), + totalQuestions = 12, + totalTimeSeconds = 30, + remainingTimeSeconds = 10, ), onSelect = {}, ) diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt index 9e2ebad..19b5fc6 100644 --- a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt @@ -11,20 +11,33 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @HiltViewModel class QuizScreenViewModel @Inject constructor( private val getQuizUseCase: GetQuizUseCase, ) : ViewModel() { private val _selectedChoiceIndex = MutableStateFlow(null) + private val _remainingTimeSeconds = MutableStateFlow(null) + private var timerJob: kotlinx.coroutines.Job? = null + val uiState: StateFlow = combine( suspend { getQuizUseCase() }.asFlow(), _selectedChoiceIndex, - ) { quiz, selectedChoiceIndex -> + _remainingTimeSeconds, + ) { quiz, selectedChoiceIndex, remaining -> + val currentQuestion = quiz.questions.first() + val totalSeconds = (currentQuestion.time ?: 30.seconds).inWholeSeconds.toInt() QuizUiState( - currentQuestion = quiz.questions.first(), + currentQuestion = currentQuestion, answer = selectedChoiceIndex?.let { AnswerUiState(it) }, + currentQuestionIndex = 0, + totalQuestions = quiz.questions.size, + totalTimeSeconds = totalSeconds, + remainingTimeSeconds = remaining ?: totalSeconds, ) }.stateIn( scope = viewModelScope, @@ -33,13 +46,45 @@ class QuizScreenViewModel @Inject constructor( ) fun onChoiceSelected(index: Int) { + timerJob?.cancel() _selectedChoiceIndex.value = index } + + init { + // Start countdown for the first question + viewModelScope.launch { + val quiz = getQuizUseCase() + val totalSeconds = quiz.questions.first().time?.inWholeSeconds?.toInt() + startCountdown(totalSeconds) + } + } + + private fun startCountdown(totalSeconds: Int?) { + timerJob?.cancel() + if (totalSeconds == null || totalSeconds <= 0) return + _remainingTimeSeconds.value = totalSeconds + timerJob = viewModelScope.launch { + var remaining = totalSeconds + while (remaining > 0) { + delay(1000) + remaining -= 1 + _remainingTimeSeconds.value = remaining + } + // Time out: reveal answers without a selection + if (_selectedChoiceIndex.value == null) { + _selectedChoiceIndex.value = -1 + } + } + } } data class QuizUiState( val currentQuestion: Question? = null, val answer: AnswerUiState? = null, + val currentQuestionIndex: Int = 0, + val totalQuestions: Int = 0, + val totalTimeSeconds: Int = -1, + val remainingTimeSeconds: Int = -1, ) data class AnswerUiState(