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 6d0adee..1146df7 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,26 +1,13 @@ package dev.adriankuta.kahootquiz.ui.quiz import androidx.compose.animation.animateContentSize -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 -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth 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.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape @@ -32,33 +19,22 @@ 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.clip -import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.text.HtmlCompat import androidx.hilt.navigation.compose.hiltViewModel 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.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 -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.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 dev.adriankuta.kahootquiz.core.designsystem.R as DesignR @@ -219,265 +195,6 @@ private fun LazyListScope.choices( } } -@Composable -private 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), - ) - } - } -} - -@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) - .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), - ) - } -} - -@Composable -private fun Choices( - choices: List, - 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 - } - - // 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, - 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, - (-8).dp, - ), - ) - Text( - text = choice.answer ?: "", - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.Center), - color = contrastiveTo(backgroundColor), - ) - } -} - -@Composable -private 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) - .clipToBounds(), - color = Color.White, - ) - } -} - @Preview @Composable private fun QuizScreenPreview() { diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/Choices.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/Choices.kt new file mode 100644 index 0000000..8ea81f1 --- /dev/null +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/Choices.kt @@ -0,0 +1,176 @@ +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.offset +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, + 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), + ) + } +} diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/QuestionContent.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/QuestionContent.kt new file mode 100644 index 0000000..9ec3698 --- /dev/null +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/QuestionContent.kt @@ -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.heightIn +import androidx.compose.foundation.layout.height +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.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.draw.clip +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), + ) + } +} diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/TimerBar.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/TimerBar.kt new file mode 100644 index 0000000..fde5b1c --- /dev/null +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/TimerBar.kt @@ -0,0 +1,51 @@ +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 androidx.compose.ui.draw.clipToBounds +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, + ) + } +} diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/Toolbar.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/Toolbar.kt new file mode 100644 index 0000000..346e995 --- /dev/null +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/components/Toolbar.kt @@ -0,0 +1,65 @@ +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.fillMaxWidth +import androidx.compose.foundation.layout.height +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), + ) + } + } +}