mirror of
				https://github.com/AdrianKuta/KahootQuiz.git
				synced 2025-10-31 00:43:40 +01:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			710dedb0cc
			...
			12638f33d8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 12638f33d8 | ||
|   | 3194d2a813 | ||
|   | 7cd3394098 | ||
|   | 7d38facda5 | ||
|   | d2fce7e7b9 | ||
|   | 41fd729271 | ||
|   | f0bd963d2d | 
| @@ -19,11 +19,12 @@ val Purple40 = Color(0xFF6650a4) | |||||||
| val PurpleGrey40 = Color(0xFF625b71) | val PurpleGrey40 = Color(0xFF625b71) | ||||||
| val Pink40 = Color(0xFF7D5260) | 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 Blue2 = Color(0xFF1368CE) | ||||||
| val Yellow3 = Color(0xFFD89E00) |  | ||||||
| val Green = Color(0xFF66BF39) | val Green = Color(0xFF66BF39) | ||||||
| val Green2 = Color(0xFF26890C) | 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) | ||||||
|   | |||||||
| @@ -1,26 +1,19 @@ | |||||||
| package dev.adriankuta.kahootquiz.ui.quiz | package dev.adriankuta.kahootquiz.ui.quiz | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.animateContentSize | ||||||
| import androidx.compose.foundation.Image | import androidx.compose.foundation.Image | ||||||
| import androidx.compose.foundation.background |  | ||||||
| import androidx.compose.foundation.clickable |  | ||||||
| import androidx.compose.foundation.layout.Arrangement |  | ||||||
| import androidx.compose.foundation.layout.Box | import androidx.compose.foundation.layout.Box | ||||||
| import androidx.compose.foundation.layout.Column |  | ||||||
| import androidx.compose.foundation.layout.PaddingValues |  | ||||||
| import androidx.compose.foundation.layout.Row |  | ||||||
| import androidx.compose.foundation.layout.Spacer | import androidx.compose.foundation.layout.Spacer | ||||||
| import androidx.compose.foundation.layout.fillMaxSize | import androidx.compose.foundation.layout.fillMaxSize | ||||||
| import androidx.compose.foundation.layout.fillMaxWidth | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
| import androidx.compose.foundation.layout.height | import androidx.compose.foundation.layout.height | ||||||
| import androidx.compose.foundation.layout.heightIn |  | ||||||
| import androidx.compose.foundation.layout.offset |  | ||||||
| import androidx.compose.foundation.layout.padding | import androidx.compose.foundation.layout.padding | ||||||
| import androidx.compose.foundation.layout.size | import androidx.compose.foundation.lazy.LazyColumn | ||||||
| import androidx.compose.foundation.layout.width | import androidx.compose.foundation.lazy.LazyListScope | ||||||
| import androidx.compose.foundation.lazy.grid.GridCells |  | ||||||
| import androidx.compose.foundation.lazy.grid.LazyVerticalGrid |  | ||||||
| import androidx.compose.foundation.lazy.grid.itemsIndexed |  | ||||||
| import androidx.compose.foundation.shape.RoundedCornerShape | import androidx.compose.foundation.shape.RoundedCornerShape | ||||||
|  | import androidx.compose.material3.ButtonDefaults | ||||||
|  | import androidx.compose.material3.CircularProgressIndicator | ||||||
|  | import androidx.compose.material3.FilledTonalButton | ||||||
| import androidx.compose.material3.Text | import androidx.compose.material3.Text | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.runtime.getValue | import androidx.compose.runtime.getValue | ||||||
| @@ -30,26 +23,18 @@ import androidx.compose.ui.graphics.Color | |||||||
| import androidx.compose.ui.layout.ContentScale | import androidx.compose.ui.layout.ContentScale | ||||||
| import androidx.compose.ui.res.painterResource | import androidx.compose.ui.res.painterResource | ||||||
| import androidx.compose.ui.res.stringResource | import androidx.compose.ui.res.stringResource | ||||||
| import androidx.compose.ui.text.style.TextAlign |  | ||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.core.text.HtmlCompat |  | ||||||
| import androidx.hilt.navigation.compose.hiltViewModel | import androidx.hilt.navigation.compose.hiltViewModel | ||||||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||||||
| import coil3.compose.AsyncImage |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Blue2 |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Green |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Green2 |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Grey | import dev.adriankuta.kahootquiz.core.designsystem.Grey | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme | import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Pink |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Red |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Red2 |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.Yellow3 |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.contrastiveTo |  | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.toAnnotatedString |  | ||||||
| import dev.adriankuta.kahootquiz.domain.models.Choice | import dev.adriankuta.kahootquiz.domain.models.Choice | ||||||
| import dev.adriankuta.kahootquiz.domain.models.Question | import dev.adriankuta.kahootquiz.domain.models.Question | ||||||
|  | import dev.adriankuta.kahootquiz.ui.quiz.components.Choices | ||||||
|  | import dev.adriankuta.kahootquiz.ui.quiz.components.QuestionContent | ||||||
|  | import dev.adriankuta.kahootquiz.ui.quiz.components.TimerBar | ||||||
|  | import dev.adriankuta.kahootquiz.ui.quiz.components.Toolbar | ||||||
| import kotlin.time.Duration.Companion.seconds | import kotlin.time.Duration.Companion.seconds | ||||||
| import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR | import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR | ||||||
|  |  | ||||||
| @@ -64,14 +49,16 @@ fun QuizScreen( | |||||||
|     QuizScreen( |     QuizScreen( | ||||||
|         uiState = uiState, |         uiState = uiState, | ||||||
|         onSelect = viewModel::onChoiceSelected, |         onSelect = viewModel::onChoiceSelected, | ||||||
|  |         onContinue = viewModel::onContinue, | ||||||
|         modifier = modifier.fillMaxSize(), |         modifier = modifier.fillMaxSize(), | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
| @Composable | @Composable | ||||||
| private fun QuizScreen( | private fun QuizScreen( | ||||||
|     uiState: QuizUiState, |     uiState: ScreenUiState, | ||||||
|     onSelect: (Int) -> Unit, |     onSelect: (Int) -> Unit, | ||||||
|  |     onContinue: () -> Unit, | ||||||
|     modifier: Modifier = Modifier, |     modifier: Modifier = Modifier, | ||||||
| ) { | ) { | ||||||
|     Box(modifier.fillMaxSize()) { |     Box(modifier.fillMaxSize()) { | ||||||
| @@ -81,244 +68,134 @@ private fun QuizScreen( | |||||||
|             contentScale = ContentScale.Crop, |             contentScale = ContentScale.Crop, | ||||||
|             modifier = Modifier.fillMaxSize(), |             modifier = Modifier.fillMaxSize(), | ||||||
|         ) |         ) | ||||||
|         Column( |         when (uiState) { | ||||||
|             modifier = Modifier |             ScreenUiState.Loading -> QuizScreenLoading() | ||||||
|                 .fillMaxWidth(), |             is ScreenUiState.Success -> QuizScreenSuccess( | ||||||
|  |                 uiState = uiState, | ||||||
|  |                 onSelect = onSelect, | ||||||
|  |                 onContinue = onContinue, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | private fun QuizScreenLoading( | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
| ) { | ) { | ||||||
|  |     CircularProgressIndicator( | ||||||
|  |         modifier = modifier, | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | private fun QuizScreenSuccess( | ||||||
|  |     uiState: ScreenUiState.Success, | ||||||
|  |     onSelect: (Int) -> Unit, | ||||||
|  |     onContinue: () -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     LazyColumn( | ||||||
|  |         modifier = modifier | ||||||
|  |             .fillMaxWidth() | ||||||
|  |             .animateContentSize(), | ||||||
|  |     ) { | ||||||
|  |         toolbar(uiState) | ||||||
|  |         questionContent(uiState) | ||||||
|  |         choices(uiState, onSelect) | ||||||
|  |         // Timer below choices | ||||||
|  |         if (uiState.selectedChoiceIndex == null && uiState.timerState.totalTimeSeconds > 0) { | ||||||
|  |             timer(uiState) | ||||||
|  |         } else { | ||||||
|  |             continueButton(uiState, onContinue) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private fun LazyListScope.toolbar( | ||||||
|  |     uiState: ScreenUiState.Success, | ||||||
|  | ) { | ||||||
|  |     item(key = "toolbar") { | ||||||
|         Toolbar( |         Toolbar( | ||||||
|             modifier = Modifier |             modifier = Modifier | ||||||
|                 .fillMaxWidth() |                 .fillMaxWidth() | ||||||
|                 .height(72.dp) |                 .height(72.dp) | ||||||
|                 .padding(8.dp), |                 .padding(8.dp), | ||||||
|  |             currentQuestionIndex = uiState.currentQuestionIndex, | ||||||
|  |             totalQuestions = uiState.totalQuestions, | ||||||
|         ) |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private fun LazyListScope.questionContent( | ||||||
|  |     uiState: ScreenUiState.Success, | ||||||
|  | ) { | ||||||
|  |     if (uiState.currentQuestion != null) { | ||||||
|  |         item(key = "question_${uiState.currentQuestionIndex}") { | ||||||
|             QuestionContent( |             QuestionContent( | ||||||
|                 question = uiState.currentQuestion ?: return, |                 question = uiState.currentQuestion, | ||||||
|                 modifier = Modifier.padding(horizontal = 8.dp), |                 modifier = Modifier | ||||||
|  |                     .padding(horizontal = 8.dp) | ||||||
|  |                     .animateItem(), | ||||||
|             ) |             ) | ||||||
|             Spacer(Modifier.height(8.dp)) |             Spacer(Modifier.height(8.dp)) | ||||||
|             Choices( |  | ||||||
|                 choices = uiState.currentQuestion.choices ?: emptyList(), // TODO remove empty list |  | ||||||
|                 answer = uiState.answer, |  | ||||||
|                 onSelect = onSelect, |  | ||||||
|             ) |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @Composable | private fun LazyListScope.timer(uiState: ScreenUiState.Success) { | ||||||
| private fun Toolbar( |     item(key = "timer_${uiState.currentQuestionIndex}") { | ||||||
|     modifier: Modifier = Modifier, |         TimerBar( | ||||||
|  |             totalSeconds = uiState.timerState.totalTimeSeconds, | ||||||
|  |             remainingSeconds = uiState.timerState.remainingTimeSeconds, | ||||||
|  |             modifier = Modifier.padding(8.dp), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private fun LazyListScope.continueButton( | ||||||
|  |     uiState: ScreenUiState.Success, | ||||||
|  |     onContinue: () -> Unit, | ||||||
| ) { | ) { | ||||||
|  |     item(key = "continue_${uiState.currentQuestionIndex}") { | ||||||
|         Box( |         Box( | ||||||
|         modifier = modifier, |             modifier = Modifier.fillMaxWidth(), | ||||||
|         ) { |         ) { | ||||||
|         Text( |             FilledTonalButton( | ||||||
|             text = "2/24", |                 onClick = onContinue, | ||||||
|             modifier = Modifier |                 colors = ButtonDefaults.filledTonalButtonColors().copy( | ||||||
|                 .align(Alignment.CenterStart) |                     containerColor = Grey, | ||||||
|                 .background( |                     contentColor = Color.Black, | ||||||
|                     color = Grey, |                 ), | ||||||
|                     shape = RoundedCornerShape(60.dp), |  | ||||||
|                 ) |  | ||||||
|                 .padding(horizontal = 8.dp, vertical = 4.dp), |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         Row( |  | ||||||
|             modifier = Modifier |  | ||||||
|                 .align(Alignment.Center) |  | ||||||
|                 .background( |  | ||||||
|                     color = Grey, |  | ||||||
|                     shape = RoundedCornerShape(60.dp), |  | ||||||
|                 ) |  | ||||||
|                 .padding(horizontal = 8.dp, vertical = 4.dp), |  | ||||||
|         ) { |  | ||||||
|             Image( |  | ||||||
|                 painter = painterResource(id = DesignR.drawable.ic_type), |  | ||||||
|                 contentDescription = "", |  | ||||||
|                 modifier = Modifier.size(24.dp), |  | ||||||
|             ) |  | ||||||
|             Spacer(Modifier.width(4.dp)) |  | ||||||
|             Text( |  | ||||||
|                 text = stringResource(R.string.quiz), |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @Composable |  | ||||||
| private fun QuestionContent( |  | ||||||
|     question: Question, |  | ||||||
|     modifier: Modifier = Modifier, |  | ||||||
| ) { |  | ||||||
|     Column( |  | ||||||
|         modifier = modifier, |  | ||||||
|     ) { |  | ||||||
|         AsyncImage( |  | ||||||
|             model = question.image, |  | ||||||
|             contentDescription = question.imageMetadata?.altText, |  | ||||||
|             contentScale = ContentScale.FillWidth, |  | ||||||
|             modifier = Modifier |  | ||||||
|                 .fillMaxWidth() |  | ||||||
|                 .heightIn(min = 200.dp), |  | ||||||
|         ) |  | ||||||
|         Spacer(Modifier.height(16.dp)) |  | ||||||
|         Text( |  | ||||||
|             text = HtmlCompat.fromHtml( |  | ||||||
|                 question.question ?: "", |  | ||||||
|                 HtmlCompat.FROM_HTML_MODE_COMPACT, |  | ||||||
|             ).toAnnotatedString(), |  | ||||||
|             textAlign = TextAlign.Center, |  | ||||||
|             modifier = Modifier |  | ||||||
|                 .fillMaxWidth() |  | ||||||
|                 .background( |  | ||||||
|                     color = Color.White, |  | ||||||
|                 shape = RoundedCornerShape(4.dp), |                 shape = RoundedCornerShape(4.dp), | ||||||
|  |                 modifier = Modifier.align(Alignment.Center), | ||||||
|  |             ) { | ||||||
|  |                 Text( | ||||||
|  |                     text = stringResource(R.string.continue_text), | ||||||
|                 ) |                 ) | ||||||
|                 .padding(horizontal = 8.dp, vertical = 16.dp), |             } | ||||||
|         ) |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @Composable | private fun LazyListScope.choices( | ||||||
| private fun Choices( |     uiState: ScreenUiState.Success, | ||||||
|     choices: List<Choice>, |  | ||||||
|     onSelect: (Int) -> Unit, |     onSelect: (Int) -> Unit, | ||||||
|     answer: AnswerUiState?, |  | ||||||
|     modifier: Modifier = Modifier, |  | ||||||
| ) { | ) { | ||||||
|     LazyVerticalGrid( |     uiState.currentQuestion?.choices?.let { choicesList -> | ||||||
|         columns = GridCells.Fixed(2), |         if (choicesList.isNotEmpty()) { | ||||||
|         contentPadding = PaddingValues(8.dp), |             item(key = "choices_${uiState.currentQuestionIndex}") { | ||||||
|         horizontalArrangement = Arrangement.spacedBy(8.dp), |                 Choices( | ||||||
|         verticalArrangement = Arrangement.spacedBy(8.dp), |                     choices = choicesList, | ||||||
|         modifier = modifier, |                     selectedChoiceIndex = uiState.selectedChoiceIndex, | ||||||
|     ) { |                     onSelect = onSelect, | ||||||
|         itemsIndexed(choices) { index, choice -> |                     modifier = Modifier.padding(8.dp), | ||||||
|             ChoiceItem( |  | ||||||
|                 choice = choice, |  | ||||||
|                 index = index, |  | ||||||
|                 answer = answer, |  | ||||||
|                 onClick = { onSelect(index) }, |  | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @Composable |  | ||||||
| private fun ChoiceItem( |  | ||||||
|     choice: Choice, |  | ||||||
|     onClick: () -> Unit, |  | ||||||
|     index: Int, |  | ||||||
|     answer: AnswerUiState?, |  | ||||||
| ) { |  | ||||||
|     if (answer != null) { |  | ||||||
|         ChoiceItemRevealed( |  | ||||||
|             choice = choice, |  | ||||||
|             index = index, |  | ||||||
|             isSelected = answer.selectedChoiceIndex == index, |  | ||||||
|         ) |  | ||||||
|     } else { |  | ||||||
|         ChoiceItemDefault( |  | ||||||
|             choice = choice, |  | ||||||
|             index = index, |  | ||||||
|             onClick = onClick, |  | ||||||
|         ) |  | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
| @Composable |  | ||||||
| private fun ChoiceItemDefault( |  | ||||||
|     choice: Choice, |  | ||||||
|     index: Int, |  | ||||||
|     onClick: () -> Unit, |  | ||||||
| ) { |  | ||||||
|     val backgroundColor = when (index) { |  | ||||||
|         0 -> Red2 |  | ||||||
|         1 -> Blue2 |  | ||||||
|         2 -> Yellow3 |  | ||||||
|         3 -> Green2 |  | ||||||
|         else -> Color.Gray |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // TODO Add icons |  | ||||||
|     val icon = when (index) { |  | ||||||
|         0 -> DesignR.drawable.ic_triangle |  | ||||||
|         1 -> DesignR.drawable.ic_diamond |  | ||||||
|         2 -> DesignR.drawable.ic_circle |  | ||||||
|         else -> DesignR.drawable.ic_square |  | ||||||
|     } |  | ||||||
|     Box( |  | ||||||
|         modifier = Modifier |  | ||||||
|             .background(backgroundColor, shape = RoundedCornerShape(4.dp)) |  | ||||||
|             .height(100.dp) |  | ||||||
|             .clickable( |  | ||||||
|                 onClick = onClick, |  | ||||||
|             ), |  | ||||||
|     ) { |  | ||||||
|         Image( |  | ||||||
|             painter = painterResource(id = icon), |  | ||||||
|             contentDescription = null, |  | ||||||
|             modifier = Modifier |  | ||||||
|                 .padding(8.dp) |  | ||||||
|                 .size(32.dp), |  | ||||||
|         ) |  | ||||||
|         Text( |  | ||||||
|             text = choice.answer ?: "", |  | ||||||
|             textAlign = TextAlign.Center, |  | ||||||
|             modifier = Modifier.align(Alignment.Center), |  | ||||||
|             color = contrastiveTo(backgroundColor), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @Composable |  | ||||||
| private fun ChoiceItemRevealed( |  | ||||||
|     choice: Choice, |  | ||||||
|     index: Int, |  | ||||||
|     isSelected: Boolean, |  | ||||||
| ) { |  | ||||||
|     val backgroundColor = when { |  | ||||||
|         isSelected && !choice.correct -> Red |  | ||||||
|         choice.correct -> Green |  | ||||||
|         else -> Pink |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     val icon = if (choice.correct) { |  | ||||||
|         DesignR.drawable.ic_correct |  | ||||||
|     } else { |  | ||||||
|         DesignR.drawable.ic_wrong |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     val alignment = if (index % 2 == 0) { |  | ||||||
|         Alignment.TopStart |  | ||||||
|     } else { |  | ||||||
|         Alignment.TopEnd |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Box( |  | ||||||
|         modifier = Modifier |  | ||||||
|             .background(backgroundColor, shape = RoundedCornerShape(4.dp)) |  | ||||||
|             .height(100.dp), |  | ||||||
|     ) { |  | ||||||
|         Image( |  | ||||||
|             painter = painterResource(icon), |  | ||||||
|             contentDescription = null, |  | ||||||
|             modifier = Modifier |  | ||||||
|                 .align(alignment) |  | ||||||
|                 .offset( |  | ||||||
|                     x = if (alignment == Alignment.TopStart) (-8).dp else (8).dp, |  | ||||||
|                     (-8).dp, |  | ||||||
|                 ), |  | ||||||
|         ) |  | ||||||
|         Text( |  | ||||||
|             text = choice.answer ?: "", |  | ||||||
|             textAlign = TextAlign.Center, |  | ||||||
|             modifier = Modifier.align(Alignment.Center), |  | ||||||
|             color = contrastiveTo(backgroundColor), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @Preview | @Preview | ||||||
| @Composable | @Composable | ||||||
| @@ -340,10 +217,13 @@ private fun QuizScreenPreview() { | |||||||
|             imageMetadata = null, |             imageMetadata = null, | ||||||
|         ) |         ) | ||||||
|         QuizScreen( |         QuizScreen( | ||||||
|             uiState = QuizUiState( |             uiState = ScreenUiState.Success( | ||||||
|                 currentQuestion = sampleQuestion, |                 currentQuestion = sampleQuestion, | ||||||
|  |                 selectedChoiceIndex = null, | ||||||
|  |                 totalQuestions = 12, | ||||||
|             ), |             ), | ||||||
|             onSelect = {}, |             onSelect = {}, | ||||||
|  |             onContinue = {}, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -368,13 +248,13 @@ private fun QuizScreenRevealedAnswerPreview() { | |||||||
|             imageMetadata = null, |             imageMetadata = null, | ||||||
|         ) |         ) | ||||||
|         QuizScreen( |         QuizScreen( | ||||||
|             uiState = QuizUiState( |             uiState = ScreenUiState.Success( | ||||||
|                 currentQuestion = sampleQuestion, |                 currentQuestion = sampleQuestion, | ||||||
|                 answer = AnswerUiState( |  | ||||||
|                 selectedChoiceIndex = 1, |                 selectedChoiceIndex = 1, | ||||||
|                 ), |                 totalQuestions = 12, | ||||||
|             ), |             ), | ||||||
|             onSelect = {}, |             onSelect = {}, | ||||||
|  |             onContinue = {}, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,44 +4,154 @@ import androidx.lifecycle.ViewModel | |||||||
| import androidx.lifecycle.viewModelScope | import androidx.lifecycle.viewModelScope | ||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
| import dev.adriankuta.kahootquiz.domain.models.Question | import dev.adriankuta.kahootquiz.domain.models.Question | ||||||
|  | import dev.adriankuta.kahootquiz.domain.models.Quiz | ||||||
| import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase | import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import kotlinx.coroutines.delay | ||||||
| import kotlinx.coroutines.flow.MutableStateFlow | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
| import kotlinx.coroutines.flow.SharingStarted | import kotlinx.coroutines.flow.SharingStarted | ||||||
| import kotlinx.coroutines.flow.StateFlow | import kotlinx.coroutines.flow.StateFlow | ||||||
| import kotlinx.coroutines.flow.asFlow |  | ||||||
| import kotlinx.coroutines.flow.combine | import kotlinx.coroutines.flow.combine | ||||||
|  | import kotlinx.coroutines.flow.flow | ||||||
| import kotlinx.coroutines.flow.stateIn | import kotlinx.coroutines.flow.stateIn | ||||||
|  | import kotlinx.coroutines.launch | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
|  |  | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| class QuizScreenViewModel @Inject constructor( | class QuizScreenViewModel @Inject constructor( | ||||||
|     private val getQuizUseCase: GetQuizUseCase, |     private val getQuizUseCase: GetQuizUseCase, | ||||||
| ) : ViewModel() { | ) : ViewModel() { | ||||||
|     private val _selectedChoiceIndex = MutableStateFlow<Int?>(null) |  | ||||||
|     val uiState: StateFlow<QuizUiState> = combine( |     private val quiz: StateFlow<QuizUiState> = flow { | ||||||
|         suspend { getQuizUseCase() }.asFlow(), |         emit(QuizUiState.Success(getQuizUseCase())) | ||||||
|         _selectedChoiceIndex, |     } | ||||||
|     ) { quiz, selectedChoiceIndex -> |         .stateIn( | ||||||
|         QuizUiState( |  | ||||||
|             currentQuestion = quiz.questions.first(), |  | ||||||
|             answer = selectedChoiceIndex?.let { AnswerUiState(it) }, |  | ||||||
|         ) |  | ||||||
|     }.stateIn( |  | ||||||
|             scope = viewModelScope, |             scope = viewModelScope, | ||||||
|         started = SharingStarted.WhileSubscribed(5000), |             started = SharingStarted.WhileSubscribed(5_000), | ||||||
|         initialValue = QuizUiState(), |             initialValue = QuizUiState.Loading, | ||||||
|  |         ) | ||||||
|  |     private val _selectedChoiceIndex = MutableStateFlow<Int?>(null) | ||||||
|  |     private val _remainingTimeSeconds = MutableStateFlow(0) | ||||||
|  |     private val _currentQuestionIndex = MutableStateFlow(0) | ||||||
|  |     private var timerJob: Job? = null | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         // Start timer when the first question is displayed (on initial quiz load) | ||||||
|  |         viewModelScope.launch { | ||||||
|  |             quiz.collect { quizState -> | ||||||
|  |                 if (quizState is QuizUiState.Success) { | ||||||
|  |                     // Start only if timer hasn't been started yet and we are on the first question | ||||||
|  |                     if (timerJob == null && _currentQuestionIndex.value == 0) { | ||||||
|  |                         val firstQuestionTime = | ||||||
|  |                             quizState.quiz.questions.getOrNull(0)?.time?.inWholeSeconds?.toInt() | ||||||
|  |                         startCountdown(firstQuestionTime) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val uiState: StateFlow<ScreenUiState> = combine( | ||||||
|  |         quiz, | ||||||
|  |         _selectedChoiceIndex, | ||||||
|  |         _remainingTimeSeconds, | ||||||
|  |         _currentQuestionIndex, | ||||||
|  |     ) { quizState, selectedChoiceIndex, remainingTimeSeconds, currentQuestionIndex -> | ||||||
|  |         when (quizState) { | ||||||
|  |             QuizUiState.Loading -> ScreenUiState.Loading | ||||||
|  |             is QuizUiState.Success -> { | ||||||
|  |                 val currentQuestion = quizState.quiz.questions.getOrNull(currentQuestionIndex) | ||||||
|  |  | ||||||
|  |                 ScreenUiState.Success( | ||||||
|  |                     currentQuestion = currentQuestion, | ||||||
|  |                     selectedChoiceIndex = selectedChoiceIndex, | ||||||
|  |                     currentQuestionIndex = currentQuestionIndex, | ||||||
|  |                     totalQuestions = quizState.quiz.questions.size, | ||||||
|  |                     timerState = TimerState( | ||||||
|  |                         remainingTimeSeconds = remainingTimeSeconds, | ||||||
|  |                         totalTimeSeconds = currentQuestion?.time?.inWholeSeconds?.toInt() ?: 0, | ||||||
|  |                     ), | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |         .stateIn( | ||||||
|  |             scope = viewModelScope, | ||||||
|  |             started = SharingStarted.WhileSubscribed(5_000), | ||||||
|  |             initialValue = ScreenUiState.Loading, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     fun onChoiceSelected(index: Int) { |     fun onChoiceSelected(index: Int) { | ||||||
|  |         timerJob?.cancel() | ||||||
|  |         timerJob = null | ||||||
|         _selectedChoiceIndex.value = index |         _selectedChoiceIndex.value = index | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fun onContinue() { | ||||||
|  |         val quizState = quiz.value | ||||||
|  |         if (quizState is QuizUiState.Success) { | ||||||
|  |             val total = quizState.quiz.questions.size | ||||||
|  |             val current = _currentQuestionIndex.value | ||||||
|  |             val nextIndex = current + 1 | ||||||
|  |             if (nextIndex < total) { | ||||||
|  |                 _selectedChoiceIndex.value = null | ||||||
|  |                 _currentQuestionIndex.value = nextIndex | ||||||
|  |                 val nextQuestionTime = | ||||||
|  |                     quizState.quiz.questions[nextIndex].time?.inWholeSeconds?.toInt() | ||||||
|  |                 startCountdown(nextQuestionTime) | ||||||
|  |             } else { | ||||||
|  |                 // Last question reached: stop timer and keep state (could navigate to results in the future) | ||||||
|  |                 timerJob?.cancel() | ||||||
|  |                 timerJob = null | ||||||
|  |                 _remainingTimeSeconds.value = 0 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| data class QuizUiState( |     private fun startCountdown(totalSeconds: Int?) { | ||||||
|     val currentQuestion: Question? = null, |         timerJob?.cancel() | ||||||
|     val answer: AnswerUiState? = null, |         if (totalSeconds == null || totalSeconds <= 0) { | ||||||
| ) |             _remainingTimeSeconds.value = 0 | ||||||
|  |             timerJob = null | ||||||
|  |             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 | ||||||
|  |             } | ||||||
|  |             timerJob = null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| data class AnswerUiState( | sealed interface QuizUiState { | ||||||
|     val selectedChoiceIndex: Int, |     data object Loading : QuizUiState | ||||||
|  |     data class Success( | ||||||
|  |         val quiz: Quiz, | ||||||
|  |     ) : QuizUiState | ||||||
|  | } | ||||||
|  |  | ||||||
|  | sealed interface ScreenUiState { | ||||||
|  |     data object Loading : ScreenUiState | ||||||
|  |     data class Success( | ||||||
|  |         val currentQuestion: Question? = null, | ||||||
|  |         val selectedChoiceIndex: Int? = null, | ||||||
|  |         val currentQuestionIndex: Int = 0, | ||||||
|  |         val totalQuestions: Int = 0, | ||||||
|  |         val timerState: TimerState = TimerState(), | ||||||
|  |     ) : ScreenUiState | ||||||
|  | } | ||||||
|  |  | ||||||
|  | data class TimerState( | ||||||
|  |     val remainingTimeSeconds: Int = 0, | ||||||
|  |     val totalTimeSeconds: Int = 0, | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -0,0 +1,175 @@ | |||||||
|  | package dev.adriankuta.kahootquiz.ui.quiz.components | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.Image | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.clickable | ||||||
|  | import androidx.compose.foundation.layout.Arrangement | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.FlowRow | ||||||
|  | import androidx.compose.foundation.layout.height | ||||||
|  | import androidx.compose.foundation.layout.offset | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.layout.size | ||||||
|  | import androidx.compose.foundation.shape.RoundedCornerShape | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.res.painterResource | ||||||
|  | import androidx.compose.ui.text.style.TextAlign | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Blue2 | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Green | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Green2 | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Pink | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Red | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Red2 | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Yellow3 | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.contrastiveTo | ||||||
|  | import dev.adriankuta.kahootquiz.domain.models.Choice | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun Choices( | ||||||
|  |     choices: List<Choice>, | ||||||
|  |     onSelect: (Int) -> Unit, | ||||||
|  |     selectedChoiceIndex: Int?, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     FlowRow( | ||||||
|  |         maxItemsInEachRow = 2, | ||||||
|  |         modifier = modifier, | ||||||
|  |         horizontalArrangement = Arrangement.spacedBy(8.dp), | ||||||
|  |         verticalArrangement = Arrangement.spacedBy(8.dp), | ||||||
|  |     ) { | ||||||
|  |         choices.forEachIndexed { index, choice -> | ||||||
|  |             ChoiceItem( | ||||||
|  |                 choice = choice, | ||||||
|  |                 index = index, | ||||||
|  |                 selectedChoiceIndex = selectedChoiceIndex, | ||||||
|  |                 onClick = { onSelect(index) }, | ||||||
|  |                 modifier = Modifier.weight(1f), | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | private fun ChoiceItem( | ||||||
|  |     choice: Choice, | ||||||
|  |     onClick: () -> Unit, | ||||||
|  |     index: Int, | ||||||
|  |     selectedChoiceIndex: Int?, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     if (selectedChoiceIndex != null) { | ||||||
|  |         ChoiceItemRevealed( | ||||||
|  |             choice = choice, | ||||||
|  |             index = index, | ||||||
|  |             isSelected = selectedChoiceIndex == index, | ||||||
|  |             modifier = modifier, | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         ChoiceItemDefault( | ||||||
|  |             choice = choice, | ||||||
|  |             index = index, | ||||||
|  |             onClick = onClick, | ||||||
|  |             modifier = modifier, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | private fun ChoiceItemDefault( | ||||||
|  |     choice: Choice, | ||||||
|  |     index: Int, | ||||||
|  |     onClick: () -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     val backgroundColor = when (index) { | ||||||
|  |         0 -> Red2 | ||||||
|  |         1 -> Blue2 | ||||||
|  |         2 -> Yellow3 | ||||||
|  |         3 -> Green2 | ||||||
|  |         else -> Color.Gray | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val icon = when (index) { | ||||||
|  |         0 -> DesignR.drawable.ic_triangle | ||||||
|  |         1 -> DesignR.drawable.ic_diamond | ||||||
|  |         2 -> DesignR.drawable.ic_circle | ||||||
|  |         else -> DesignR.drawable.ic_square | ||||||
|  |     } | ||||||
|  |     Box( | ||||||
|  |         modifier = modifier | ||||||
|  |             .background(backgroundColor, shape = RoundedCornerShape(4.dp)) | ||||||
|  |             .height(100.dp) | ||||||
|  |             .clickable( | ||||||
|  |                 onClick = onClick, | ||||||
|  |             ), | ||||||
|  |     ) { | ||||||
|  |         Image( | ||||||
|  |             painter = painterResource(id = icon), | ||||||
|  |             contentDescription = null, | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .padding(8.dp) | ||||||
|  |                 .size(32.dp), | ||||||
|  |         ) | ||||||
|  |         Text( | ||||||
|  |             text = choice.answer ?: "", | ||||||
|  |             textAlign = TextAlign.Center, | ||||||
|  |             modifier = Modifier.align(Alignment.Center), | ||||||
|  |             color = contrastiveTo(backgroundColor), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | private fun ChoiceItemRevealed( | ||||||
|  |     choice: Choice, | ||||||
|  |     index: Int, | ||||||
|  |     isSelected: Boolean, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     val backgroundColor = when { | ||||||
|  |         isSelected && !choice.correct -> Red | ||||||
|  |         choice.correct -> Green | ||||||
|  |         else -> Pink | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val icon = if (choice.correct) { | ||||||
|  |         DesignR.drawable.ic_correct | ||||||
|  |     } else { | ||||||
|  |         DesignR.drawable.ic_wrong | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val alignment = if (index % 2 == 0) { | ||||||
|  |         Alignment.TopStart | ||||||
|  |     } else { | ||||||
|  |         Alignment.TopEnd | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Box( | ||||||
|  |         modifier = modifier | ||||||
|  |             .background(backgroundColor, shape = RoundedCornerShape(4.dp)) | ||||||
|  |             .height(100.dp), | ||||||
|  |     ) { | ||||||
|  |         Image( | ||||||
|  |             painter = painterResource(icon), | ||||||
|  |             contentDescription = null, | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .align(alignment) | ||||||
|  |                 .offset( | ||||||
|  |                     x = if (alignment == Alignment.TopStart) (-8).dp else (8).dp, | ||||||
|  |                     y = (-8).dp, | ||||||
|  |                 ), | ||||||
|  |         ) | ||||||
|  |         Text( | ||||||
|  |             text = choice.answer ?: "", | ||||||
|  |             textAlign = TextAlign.Center, | ||||||
|  |             modifier = Modifier.align(Alignment.Center), | ||||||
|  |             color = contrastiveTo(backgroundColor), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,60 @@ | |||||||
|  | package dev.adriankuta.kahootquiz.ui.quiz.components | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.layout.Column | ||||||
|  | import androidx.compose.foundation.layout.Spacer | ||||||
|  | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
|  | import androidx.compose.foundation.layout.height | ||||||
|  | import androidx.compose.foundation.layout.heightIn | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.shape.RoundedCornerShape | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.draw.clip | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.layout.ContentScale | ||||||
|  | import androidx.compose.ui.text.style.TextAlign | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import androidx.core.text.HtmlCompat | ||||||
|  | import coil3.compose.AsyncImage | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.toAnnotatedString | ||||||
|  | import dev.adriankuta.kahootquiz.domain.models.Question | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun QuestionContent( | ||||||
|  |     question: Question, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     Column( | ||||||
|  |         modifier = modifier, | ||||||
|  |     ) { | ||||||
|  |         AsyncImage( | ||||||
|  |             model = question.image, | ||||||
|  |             contentDescription = question.imageMetadata?.altText, | ||||||
|  |             contentScale = ContentScale.FillWidth, | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .fillMaxWidth() | ||||||
|  |                 .heightIn(min = 200.dp) | ||||||
|  |                 .clip(shape = RoundedCornerShape(4.dp)), | ||||||
|  |         ) | ||||||
|  |         Spacer(Modifier.height(16.dp)) | ||||||
|  |         val questionText = androidx.compose.runtime.remember(question.question) { | ||||||
|  |             HtmlCompat.fromHtml( | ||||||
|  |                 question.question ?: "", | ||||||
|  |                 HtmlCompat.FROM_HTML_MODE_COMPACT, | ||||||
|  |             ).toAnnotatedString() | ||||||
|  |         } | ||||||
|  |         Text( | ||||||
|  |             text = questionText, | ||||||
|  |             textAlign = TextAlign.Center, | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .fillMaxWidth() | ||||||
|  |                 .background( | ||||||
|  |                     color = Color.White, | ||||||
|  |                     shape = RoundedCornerShape(4.dp), | ||||||
|  |                 ) | ||||||
|  |                 .padding(horizontal = 8.dp, vertical = 16.dp), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,50 @@ | |||||||
|  | package dev.adriankuta.kahootquiz.ui.quiz.components | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.core.LinearEasing | ||||||
|  | import androidx.compose.animation.core.animateFloatAsState | ||||||
|  | import androidx.compose.animation.core.tween | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.shape.RoundedCornerShape | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.getValue | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Purple | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun TimerBar( | ||||||
|  |     totalSeconds: Int, | ||||||
|  |     remainingSeconds: Int, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     val target = | ||||||
|  |         if (totalSeconds <= 0) 0f else (remainingSeconds.toFloat() / totalSeconds).coerceIn(0f, 1f) | ||||||
|  |     val progress: Float by animateFloatAsState( | ||||||
|  |         targetValue = target, | ||||||
|  |         label = "Timer", | ||||||
|  |         animationSpec = tween(durationMillis = 1000, easing = LinearEasing), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     Box( | ||||||
|  |         modifier = modifier | ||||||
|  |             .fillMaxWidth(progress.coerceIn(0f, 1f)) | ||||||
|  |             .background( | ||||||
|  |                 color = Purple, | ||||||
|  |                 shape = RoundedCornerShape(percent = 50), | ||||||
|  |             ), | ||||||
|  |     ) { | ||||||
|  |         Text( | ||||||
|  |             text = "$remainingSeconds", | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .align(Alignment.CenterEnd) | ||||||
|  |                 .padding(end = 8.dp), | ||||||
|  |             color = Color.White, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,63 @@ | |||||||
|  | package dev.adriankuta.kahootquiz.ui.quiz.components | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.Image | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.Row | ||||||
|  | import androidx.compose.foundation.layout.Spacer | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.layout.size | ||||||
|  | import androidx.compose.foundation.layout.width | ||||||
|  | import androidx.compose.foundation.shape.RoundedCornerShape | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.res.painterResource | ||||||
|  | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.Grey | ||||||
|  | import dev.adriankuta.kahootquiz.ui.quiz.R | ||||||
|  | import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun Toolbar( | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     currentQuestionIndex: Int = 0, | ||||||
|  |     totalQuestions: Int = 0, | ||||||
|  | ) { | ||||||
|  |     Box( | ||||||
|  |         modifier = modifier, | ||||||
|  |     ) { | ||||||
|  |         Text( | ||||||
|  |             text = "${currentQuestionIndex + 1}/$totalQuestions", | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .align(Alignment.CenterStart) | ||||||
|  |                 .background( | ||||||
|  |                     color = Grey, | ||||||
|  |                     shape = RoundedCornerShape(percent = 50), | ||||||
|  |                 ) | ||||||
|  |                 .padding(horizontal = 8.dp, vertical = 4.dp), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         Row( | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .align(Alignment.Center) | ||||||
|  |                 .background( | ||||||
|  |                     color = Grey, | ||||||
|  |                     shape = RoundedCornerShape(percent = 50), | ||||||
|  |                 ) | ||||||
|  |                 .padding(horizontal = 8.dp, vertical = 4.dp), | ||||||
|  |         ) { | ||||||
|  |             Image( | ||||||
|  |                 painter = painterResource(id = DesignR.drawable.ic_type), | ||||||
|  |                 contentDescription = null, | ||||||
|  |                 modifier = Modifier.size(24.dp), | ||||||
|  |             ) | ||||||
|  |             Spacer(Modifier.width(4.dp)) | ||||||
|  |             Text( | ||||||
|  |                 text = stringResource(R.string.quiz), | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <resources> | <resources> | ||||||
|     <string name="quiz">Quiz</string> |     <string name="quiz">Quiz</string> | ||||||
|  |     <string name="continue_text">Continue</string> | ||||||
| </resources> | </resources> | ||||||
		Reference in New Issue
	
	Block a user