mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-16 18:14:22 +02:00
feat: Implement interactive quiz screen with answer revealed state
This commit enhances the `QuizScreen` to be interactive, allowing users to select choices and view revealed answers. It also introduces HTML parsing for question text and adds new design elements like icons and colors. Key changes: - **UI Layer (`ui:quiz` module):** - `QuizScreen`: - Now takes an `onSelect` callback to handle choice selection. - `Choices` composable updated to display choices in a `LazyVerticalGrid` and handles click events. - Introduced `ChoiceItem` which branches into `ChoiceItemDefault` (for selectable choices) and `ChoiceItemRevealed` (for displaying correct/incorrect answers). - `ChoiceItemDefault` displays choices with background colors and icons based on their index. - `ChoiceItemRevealed` displays choices with background colors indicating correctness (green for correct, red for incorrect selected, pink for incorrect unselected) and appropriate icons (tick for correct, cross for wrong). - `QuestionContent` now parses HTML in the question text using `HtmlCompat` and a new `toAnnotatedString` extension. - Image loading in `QuestionContent` uses `ContentScale.FillWidth` and `heightIn(min = 200.dp)`. - Added a new preview `QuizScreenRevealedAnswerPreview` to showcase the revealed answer state. - `QuizScreenViewModel`: - Now manages `_selectedChoiceIndex` to track the user's answer. - `uiState` is now a combination of the fetched quiz and the `_selectedChoiceIndex`, producing `QuizUiState` which includes an `AnswerUiState`. - `AnswerUiState` holds the `selectedChoiceIndex`. - Implemented `onChoiceSelected(index: Int)` to update the selected choice. - **Design System (`core:designsystem` module):** - Added `TextUtils.kt` with a `Spanned.toAnnotatedString()` extension function to convert HTML formatted text (from `HtmlCompat`) into Jetpack Compose `AnnotatedString`. - Added new color definitions: `Pink`, `Red`, `Red2`, `Blue2`, `Yellow3`, `Green`, `Green2`. - Added a `contrastiveTo(color: Color)` utility function to determine a contrasting text color (black or white) for a given background color. - Added new vector drawables for choice shapes and correctness indicators: - `ic_circle.xml` - `ic_correct.xml` - `ic_diamond.xml` - `ic_square.xml` - `ic_triangle.xml` - `ic_wrong.xml` - Added Detekt configuration file (`config/detekt/detekt.yml`) for the design system module. - **Domain Layer (`domain` module):** - `Question.image` is now nullable (`String?`). - `Choice.correct` is now non-nullable (`Boolean`). - **Network Layer (`core:network` module):** - `ChoiceDto.correct` is now non-nullable (`Boolean`) to align with the domain model. - **Project Configuration:** - Added `.editorconfig` with Kotlin specific trailing comma settings. - Minor reordering of dependencies in `gradle/libs.versions.toml`.
This commit is contained in:
33
core/designsystem/config/detekt/detekt.yml
Normal file
33
core/designsystem/config/detekt/detekt.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
# Exceptions for compose. See https://detekt.dev/docs/introduction/compose
|
||||
naming:
|
||||
FunctionNaming:
|
||||
functionPattern: '[a-zA-Z][a-zA-Z0-9]*'
|
||||
|
||||
TopLevelPropertyNaming:
|
||||
constantPattern: '[A-Z][A-Za-z0-9]*'
|
||||
|
||||
complexity:
|
||||
LongParameterList:
|
||||
ignoreAnnotated: ['Composable']
|
||||
TooManyFunctions:
|
||||
ignoreAnnotatedFunctions: ['Preview']
|
||||
|
||||
style:
|
||||
MagicNumber:
|
||||
ignorePropertyDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotated: ['Composable']
|
||||
|
||||
UnusedPrivateMember:
|
||||
ignoreAnnotated: ['Composable']
|
||||
|
||||
# Deviations from defaults
|
||||
formatting:
|
||||
TrailingCommaOnCallSite:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
useTrailingCommaOnCallSite: true
|
||||
TrailingCommaOnDeclarationSite:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
useTrailingCommaOnDeclarationSite: true
|
@@ -1,6 +1,15 @@
|
||||
package dev.adriankuta.kahootquiz.core.designsystem
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
|
||||
@Composable
|
||||
fun contrastiveTo(color: Color): Color = if (color.luminance() < 0.5) {
|
||||
Color.White
|
||||
} else {
|
||||
Color.Black
|
||||
}
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
@@ -10,4 +19,11 @@ val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
||||
val Grey = Color(0xFFFAFAFA)
|
||||
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)
|
||||
|
@@ -0,0 +1,48 @@
|
||||
package dev.adriankuta.kahootquiz.core.designsystem
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
|
||||
fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
|
||||
val spanned = this@toAnnotatedString
|
||||
append(spanned.toString())
|
||||
getSpans(0, spanned.length, Any::class.java).forEach { span ->
|
||||
val start = getSpanStart(span)
|
||||
val end = getSpanEnd(span)
|
||||
when (span) {
|
||||
is StyleSpan -> when (span.style) {
|
||||
Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
|
||||
Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
|
||||
Typeface.BOLD_ITALIC -> addStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontStyle = FontStyle.Italic,
|
||||
),
|
||||
start, end,
|
||||
)
|
||||
}
|
||||
|
||||
is UnderlineSpan -> addStyle(
|
||||
SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
start,
|
||||
end,
|
||||
)
|
||||
|
||||
is ForegroundColorSpan -> addStyle(
|
||||
SpanStyle(color = Color(span.foregroundColor)),
|
||||
start,
|
||||
end,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
15
core/designsystem/src/main/res/drawable/ic_circle.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_circle.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M20,10C14.477,10 10,14.477 10,20C10,25.523 14.477,30 20,30C25.523,30 30,25.523 30,20C30,14.477 25.523,10 20,10Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M20,9.5C25.799,9.5 30.5,14.201 30.5,20C30.5,25.799 25.799,30.5 20,30.5C14.201,30.5 9.5,25.799 9.5,20C9.5,14.201 14.201,9.5 20,9.5Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
24
core/designsystem/src/main/res/drawable/ic_correct.xml
Normal file
24
core/designsystem/src/main/res/drawable/ic_correct.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#67BF38"
|
||||
android:strokeColor="#26890C"/>
|
||||
<path
|
||||
android:pathData="M25.237,12.219L28.298,14.723L18.157,27.123L11.651,21.662L14.296,18.722L17.722,21.409L25.237,12.219Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M25.554,11.831L28.615,14.336L29.001,14.653L28.685,15.039L18.545,27.44L18.224,27.832L17.836,27.507L11.33,22.045L10.933,21.713L11.279,21.328L13.925,18.388L14.237,18.041L14.604,18.329L17.645,20.713L24.85,11.902L25.167,11.515L25.554,11.831Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M25.237,12.219L28.298,14.723L18.157,27.123L11.651,21.662L14.296,18.722L17.722,21.409L25.237,12.219ZM25.554,11.831L28.615,14.336L29.001,14.653L28.685,15.039L18.545,27.44L18.224,27.832L17.836,27.507L11.33,22.045L10.933,21.713L11.279,21.328L13.925,18.388L14.237,18.041L14.604,18.329L17.645,20.713L24.85,11.902L25.167,11.515L25.554,11.831Z"/>
|
||||
</group>
|
||||
</vector>
|
15
core/designsystem/src/main/res/drawable/ic_diamond.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_diamond.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M20,8.75L8.75,20.004L20,31.25L31.25,20.001L20,8.75Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M20.354,8.396L31.604,19.647L31.957,20.001L31.604,20.354L20.354,31.604L20,31.957L19.646,31.604L8.396,20.357L8.043,20.004L8.396,19.65L19.646,8.396L20,8.043L20.354,8.396Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
15
core/designsystem/src/main/res/drawable/ic_square.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_square.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="41dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="41">
|
||||
<path
|
||||
android:pathData="M28.75,11.476H11.25V28.976H28.75V11.476Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M29.25,10.976V29.476H10.75V10.976H29.25Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
15
core/designsystem/src/main/res/drawable/ic_triangle.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_triangle.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M20,11.25L8.75,28.75H31.25L20,11.25Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M20.421,10.979L31.671,28.479L32.166,29.25H7.834L8.329,28.479L19.579,10.979L20,10.325L20.421,10.979Z"
|
||||
android:strokeAlpha="0.15"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
35
core/designsystem/src/main/res/drawable/ic_wrong.xml
Normal file
35
core/designsystem/src/main/res/drawable/ic_wrong.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||
android:fillColor="#FF3355"/>
|
||||
<path
|
||||
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FF3355"/>
|
||||
<path
|
||||
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||
android:strokeAlpha="0.15"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M12.04,24.636L15.355,27.95L19.995,23.31L24.636,27.95L27.95,24.636L23.31,19.995L27.95,15.355L24.636,12.04L19.995,16.681L15.355,12.04L12.04,15.355L16.681,19.995L12.04,24.636Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M5.853,5.721h28.284v28.284h-28.284z"/>
|
||||
<clip-path
|
||||
android:pathData="M12.04,24.636L15.355,27.95L19.995,23.31L24.636,27.95L27.95,24.636L23.31,19.995L27.95,15.355L24.636,12.04L19.995,16.681L15.355,12.04L12.04,15.355L16.681,19.995L12.04,24.636Z"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M15.355,27.95L14.648,28.657L15.355,29.365L16.062,28.657L15.355,27.95ZM12.04,24.636L11.333,23.929L10.626,24.636L11.333,25.343L12.04,24.636ZM19.995,23.31L20.702,22.603L19.995,21.896L19.288,22.603L19.995,23.31ZM24.636,27.95L23.929,28.657L24.636,29.365L25.343,28.657L24.636,27.95ZM27.95,24.636L28.657,25.343L29.365,24.636L28.657,23.929L27.95,24.636ZM23.31,19.995L22.603,19.288L21.896,19.995L22.603,20.702L23.31,19.995ZM27.95,15.355L28.657,16.062L29.365,15.355L28.657,14.648L27.95,15.355ZM24.636,12.04L25.343,11.333L24.636,10.626L23.929,11.333L24.636,12.04ZM19.995,16.681L19.288,17.388L19.995,18.095L20.702,17.388L19.995,16.681ZM15.355,12.04L16.062,11.333L15.355,10.626L14.648,11.333L15.355,12.04ZM12.04,15.355L11.333,14.648L10.626,15.355L11.333,16.062L12.04,15.355ZM16.681,19.995L17.388,20.702L18.095,19.995L17.388,19.288L16.681,19.995ZM16.062,27.243L12.748,23.929L11.333,25.343L14.648,28.657L16.062,27.243ZM19.288,22.603L14.648,27.243L16.062,28.657L20.702,24.017L19.288,22.603ZM25.343,27.243L20.702,22.603L19.288,24.017L23.929,28.657L25.343,27.243ZM27.243,23.929L23.929,27.243L25.343,28.657L28.657,25.343L27.243,23.929ZM22.603,20.702L27.243,25.343L28.657,23.929L24.017,19.288L22.603,20.702ZM27.243,14.648L22.603,19.288L24.017,20.702L28.657,16.062L27.243,14.648ZM23.929,12.748L27.243,16.062L28.657,14.648L25.343,11.333L23.929,12.748ZM20.702,17.388L25.343,12.748L23.929,11.333L19.288,15.974L20.702,17.388ZM14.648,12.748L19.288,17.388L20.702,15.974L16.062,11.333L14.648,12.748ZM12.748,16.062L16.062,12.748L14.648,11.333L11.333,14.648L12.748,16.062ZM17.388,19.288L12.748,14.648L11.333,16.062L15.974,20.702L17.388,19.288ZM12.748,25.343L17.388,20.702L15.974,19.288L11.333,23.929L12.748,25.343Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.15"/>
|
||||
</group>
|
||||
</vector>
|
@@ -24,7 +24,7 @@ data class QuestionDto(
|
||||
|
||||
data class ChoiceDto(
|
||||
val answer: String?,
|
||||
val correct: Boolean?,
|
||||
val correct: Boolean,
|
||||
val languageInfo: LanguageInfoDto?
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user