Compare commits

...

3 Commits

Author SHA1 Message Date
Adrian Kuta
710dedb0cc refactor: Format code with spotless
This commit applies automated code formatting using Spotless across multiple modules. The changes primarily involve adjustments to trailing commas, spacing, and import statements to ensure consistency with the project's coding style.

Key changes include:

- **Kotlin Files:**
    - Added trailing commas to multi-line parameter lists, argument lists, and collection literals in various Kotlin files across `app`, `core:designsystem`, `core:network`, `domain`, and `model:data` modules.
    - Standardized spacing around operators and keywords.
    - Optimized import statements in test files (`ExampleUnitTest.kt`, `ExampleInstrumentedTest.kt`).
- **XML Files:**
    - Reformatted XML attributes in drawable vector files (`ic_*.xml`) within the `core:designsystem` module for better readability.
- **JSON Files:**
    - Reformatted `sample_quiz.json` in `core:network` test resources for consistent structure.
- **Detekt Configuration:**
    - Updated `detekt.yml` files in `app` and `core:designsystem` to adjust ignored annotations lists, ensuring proper spacing (e.g., `['Composable']` to `[ 'Composable' ]`).
2025-09-03 23:46:09 +02:00
Adrian Kuta
7568abb775 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`.
2025-09-03 23:45:29 +02:00
Adrian Kuta
45550ecf76 feat: Enhance QuizScreen UI and introduce core design system module
This commit significantly revamps the `QuizScreen` UI to display question details including image and text, and introduces a new `core:designsystem` module to centralize theme, colors, typography, and drawable resources.

Key changes:

- **UI Layer (`ui:quiz` module):**
    - Updated `QuizScreen.kt`:
        - Implemented a background image for the screen.
        - Added a `Toolbar` composable to display question progress and type.
        - Created `QuestionContent` composable to show the question image (using Coil for image loading) and text.
        - Added a placeholder `Choices` composable (currently an empty `LazyVerticalGrid`).
        - Applied `fillMaxSize()` to the main `QuizScreen` modifier.
        - Included a `@Preview` for `QuizScreen` with sample data.
    - Modified `QuizScreenViewModel.kt` to update `QuizUiState` with the first `Question` from the fetched quiz.
    - Added a `quiz` string resource in `strings.xml`.
    - Added dependencies for Coil (compose and okhttp) in `ui/quiz/build.gradle.kts`.
- **Core Design System (`core:designsystem` module):**
    - Created a new Android library module `core:designsystem`.
    - Moved `Color.kt`, `Theme.kt`, and `Type.kt` from `app/src/main/java/dev/adriankuta/kahootquiz/ui/theme` to this new module.
    - Added a new `Grey` color to `Color.kt`.
    - Added `bg_image.webp` and `ic_type.xml` drawable resources.
    - Configured the module with `kahootquiz.android.library.compose` plugin.
- **App Module (`app` module):**
    - Updated `MainActivity.kt` to import `KahootQuizTheme` from the new `core.designsystem` package.
    - Added `implementation(projects.core.designsystem)` dependency in `app/build.gradle.kts`.
- **Domain Layer (`domain` module):**
    - Made several fields in `Question.kt` and `Choice.kt` nullable and provided default null values to accommodate potential missing data from the API.
    - Specifically, `Question.image` is now non-nullable (`String`).
- **Build System:**
    - Added `coilCompose` and `coilNetworkOkhttp` versions to `gradle/libs.versions.toml`.
    - Included `:core:designsystem` in `settings.gradle.kts`.
2025-09-03 21:57:18 +02:00
60 changed files with 1413 additions and 202 deletions

4
.editorconfig Executable file
View File

@@ -0,0 +1,4 @@
[*.{kt,kts}]
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true

1
.idea/gradle.xml generated
View File

@@ -26,6 +26,7 @@
<option value="$PROJECT_DIR$/build-logic" /> <option value="$PROJECT_DIR$/build-logic" />
<option value="$PROJECT_DIR$/build-logic/convention" /> <option value="$PROJECT_DIR$/build-logic/convention" />
<option value="$PROJECT_DIR$/core" /> <option value="$PROJECT_DIR$/core" />
<option value="$PROJECT_DIR$/core/designsystem" />
<option value="$PROJECT_DIR$/core/network" /> <option value="$PROJECT_DIR$/core/network" />
<option value="$PROJECT_DIR$/domain" /> <option value="$PROJECT_DIR$/domain" />
<option value="$PROJECT_DIR$/model" /> <option value="$PROJECT_DIR$/model" />

View File

@@ -20,17 +20,17 @@ android {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro",
) )
} }
} }
} }
dependencies { dependencies {
implementation(projects.ui.quiz) implementation(projects.core.designsystem)
implementation(projects.domain) implementation(projects.domain)
implementation(projects.model.data) implementation(projects.model.data)
implementation(projects.ui.quiz)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)

View File

@@ -8,18 +8,18 @@ naming:
complexity: complexity:
LongParameterList: LongParameterList:
ignoreAnnotated: ['Composable'] ignoreAnnotated: [ 'Composable' ]
TooManyFunctions: TooManyFunctions:
ignoreAnnotatedFunctions: ['Preview'] ignoreAnnotatedFunctions: [ 'Preview' ]
style: style:
MagicNumber: MagicNumber:
ignorePropertyDeclaration: true ignorePropertyDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotated: ['Composable'] ignoreAnnotated: [ 'Composable' ]
UnusedPrivateMember: UnusedPrivateMember:
ignoreAnnotated: ['Composable'] ignoreAnnotated: [ 'Composable' ]
# Deviations from defaults # Deviations from defaults
formatting: formatting:

View File

@@ -1,13 +1,11 @@
package dev.adriankuta.kahootquiz package dev.adriankuta.kahootquiz
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *

View File

@@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
@Composable @Composable
fun KahootQuizApp( fun KahootQuizApp(
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
contentWindowInsets = WindowInsets.safeDrawing, contentWindowInsets = WindowInsets.safeDrawing,

View File

@@ -16,7 +16,7 @@ fun KahootQuizNavGraph(
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = QuizRoute, startDestination = QuizRoute,
modifier = modifier modifier = modifier,
) { ) {
quizScreen() quizScreen()
} }

View File

@@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.adriankuta.kahootquiz.ui.theme.KahootQuizTheme import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {

View File

@@ -4,4 +4,4 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp @HiltAndroidApp
class MyApplication: Application() class MyApplication : Application()

View File

@@ -1,11 +0,0 @@
package dev.adriankuta.kahootquiz.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -1,9 +1,8 @@
package dev.adriankuta.kahootquiz package dev.adriankuta.kahootquiz
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.Assert.*
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
* *

View File

@@ -0,0 +1,7 @@
plugins {
alias(libs.plugins.kahootquiz.android.library.compose)
}
android {
namespace = "dev.adriankuta.kahootquiz.core.designsystem"
}

View 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

View File

@@ -0,0 +1,29 @@
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)
val Pink80 = Color(0xFFEFB8C8)
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)

View File

@@ -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,
)
}
}
}

View File

@@ -1,6 +1,5 @@
package dev.adriankuta.kahootquiz.ui.theme package dev.adriankuta.kahootquiz.core.designsystem
import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -14,13 +13,13 @@ import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = Purple80,
secondary = PurpleGrey80, secondary = PurpleGrey80,
tertiary = Pink80 tertiary = Pink80,
) )
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = Purple40,
secondary = PurpleGrey40, secondary = PurpleGrey40,
tertiary = Pink40 tertiary = Pink40,
/* Other default colors to override /* Other default colors to override
background = Color(0xFFFFFBFE), background = Color(0xFFFFFBFE),
@@ -38,7 +37,7 @@ fun KahootQuizTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit,
) { ) {
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
@@ -53,6 +52,6 @@ fun KahootQuizTheme(
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
content = content content = content,
) )
} }

View File

@@ -1,4 +1,4 @@
package dev.adriankuta.kahootquiz.ui.theme package dev.adriankuta.kahootquiz.core.designsystem
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@@ -13,8 +13,8 @@ val Typography = Typography(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp,
) ),
/* Other default text styles to override /* Other default text styles to override
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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:fillColor="#ffffff"
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" />
<path
android:fillColor="#00000000"
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:strokeWidth="1"
android:strokeAlpha="0.15"
android:strokeColor="#000000" />
</vector>

View File

@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#67BF38"
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeWidth="2"
android:strokeColor="#26890C" />
<path
android:fillColor="#ffffff"
android:pathData="M25.237,12.219L28.298,14.723L18.157,27.123L11.651,21.662L14.296,18.722L17.722,21.409L25.237,12.219Z" />
<path
android:fillColor="#00000000"
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:strokeWidth="1"
android:strokeAlpha="0.15"
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>

View 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:fillColor="#ffffff"
android:pathData="M20,8.75L8.75,20.004L20,31.25L31.25,20.001L20,8.75Z" />
<path
android:fillColor="#00000000"
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:strokeWidth="1"
android:strokeAlpha="0.15"
android:strokeColor="#000000" />
</vector>

View 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:fillColor="#ffffff"
android:pathData="M28.75,11.476H11.25V28.976H28.75V11.476Z" />
<path
android:fillColor="#00000000"
android:pathData="M29.25,10.976V29.476H10.75V10.976H29.25Z"
android:strokeWidth="1"
android:strokeAlpha="0.15"
android:strokeColor="#000000" />
</vector>

View 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:fillColor="#ffffff"
android:pathData="M20,11.25L8.75,28.75H31.25L20,11.25Z" />
<path
android:fillColor="#00000000"
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:strokeWidth="1"
android:strokeAlpha="0.15"
android:strokeColor="#000000" />
</vector>

View File

@@ -0,0 +1,50 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#F2F2F2"
android:fillType="evenOdd"
android:pathData="M12,22C6.477,22 2,17.523 2,12C2,6.477 6.477,2 12,2C17.523,2 22,6.477 22,12C22,17.523 17.523,22 12,22Z" />
<path
android:fillColor="#333333"
android:fillType="evenOdd"
android:pathData="M19.802,21.989L7.899,23.992C7.584,24.045 7.285,23.833 7.232,23.517L3.724,2.678C3.671,2.363 3.883,2.064 4.198,2.011L16.101,0.008C16.416,-0.045 16.715,0.168 16.768,0.483L20.276,21.322C20.33,21.637 20.117,21.936 19.802,21.989Z" />
<path
android:fillColor="#FAFAFA"
android:fillType="evenOdd"
android:pathData="M19.083,20.611L8.127,22.455C7.951,22.485 7.784,22.366 7.754,22.189L4.538,3.09C4.509,2.913 4.627,2.746 4.804,2.716L15.759,0.872C15.936,0.843 16.103,0.962 16.133,1.139L19.349,20.238C19.378,20.414 19.259,20.582 19.083,20.611Z" />
<path
android:fillColor="#26890C"
android:fillType="evenOdd"
android:pathData="M18.499,20.05L13.849,20.833C13.79,20.843 13.733,20.803 13.724,20.744L12.306,12.325C12.296,12.266 12.336,12.209 12.395,12.199L17.045,11.417C17.104,11.407 17.16,11.447 17.17,11.506L18.587,19.925C18.597,19.984 18.558,20.04 18.499,20.05Z" />
<path
android:fillColor="#FFA602"
android:fillType="evenOdd"
android:pathData="M12.967,20.981L8.317,21.764C8.258,21.774 8.202,21.733 8.192,21.674L6.775,13.255C6.765,13.196 6.805,13.14 6.864,13.13L11.514,12.347C11.573,12.337 11.629,12.377 11.639,12.437L13.057,20.856C13.066,20.915 13.026,20.971 12.967,20.981Z" />
<path
android:fillColor="#1368CE"
android:fillType="evenOdd"
android:pathData="M16.947,10.835L12.297,11.618C12.238,11.628 12.182,11.588 12.172,11.529L10.64,2.434C10.631,2.374 10.67,2.318 10.73,2.308L15.379,1.526C15.439,1.516 15.495,1.556 15.505,1.615L17.036,10.71C17.046,10.769 17.006,10.825 16.947,10.835Z" />
<path
android:fillColor="#E11C3C"
android:fillType="evenOdd"
android:pathData="M11.416,11.766L6.766,12.548C6.707,12.558 6.651,12.518 6.641,12.459L5.109,3.364C5.099,3.305 5.139,3.249 5.199,3.239L9.848,2.456C9.907,2.446 9.964,2.486 9.974,2.546L11.505,11.641C11.515,11.7 11.475,11.756 11.416,11.766Z" />
<path
android:fillColor="#FAFAFA"
android:fillType="evenOdd"
android:pathData="M7.317,8.677L8.107,6.504L9.544,8.316L7.317,8.677Z" />
<path
android:fillColor="#FAFAFA"
android:fillType="evenOdd"
android:pathData="M13.596,5.437L15.014,6.416L14.017,7.822L12.598,6.842L13.596,5.437Z" />
<path
android:fillColor="#FAFAFA"
android:fillType="evenOdd"
android:pathData="M10.79,16.727C10.704,16.175 10.188,15.796 9.637,15.881C9.086,15.967 8.709,16.484 8.795,17.037C8.881,17.59 9.397,17.968 9.948,17.883C10.498,17.798 10.875,17.28 10.79,16.727Z" />
<path
android:fillColor="#FAFAFA"
android:fillType="evenOdd"
android:pathData="M16.618,16.843L14.628,17.181L14.288,15.184L16.278,14.846L16.618,16.843Z" />
</vector>

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#FF3355"
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" />
<path
android:fillColor="#00000000"
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeWidth="2"
android:strokeColor="#FF3355" />
<path
android:fillColor="#00000000"
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:strokeWidth="2"
android:strokeAlpha="0.15"
android:strokeColor="#000000" />
<path
android:fillColor="#ffffff"
android:fillType="evenOdd"
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" />
<group>
<clip-path android:pathData="M5.853,5.721h28.284v28.284h-28.284z" />
<clip-path
android:fillType="evenOdd"
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" />
<path
android:fillAlpha="0.15"
android:fillColor="#000000"
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" />
</group>
</vector>

View File

@@ -5,7 +5,7 @@ package dev.adriankuta.kahootquiz.core.network.models
data class LanguageInfoDto( data class LanguageInfoDto(
val language: String?, val language: String?,
val lastUpdatedOn: Long?, val lastUpdatedOn: Long?,
val readAloudSupported: Boolean? val readAloudSupported: Boolean?,
) )
// Minimal channel info // Minimal channel info
@@ -16,5 +16,5 @@ data class ChannelDto(val id: String?)
data class PointDto( data class PointDto(
val x: Int?, val x: Int?,
val y: Int? val y: Int?,
) )

View File

@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.core.network.models
data class ContentTagsDto( data class ContentTagsDto(
val curriculumCodes: List<String>?, val curriculumCodes: List<String>?,
val generatedCurriculumCodes: List<String>? val generatedCurriculumCodes: List<String>?,
) )

View File

@@ -13,14 +13,14 @@ data class CoverMetadataDto(
val height: Int?, val height: Int?,
val extractedColors: List<ExtractedColorDto>?, val extractedColors: List<ExtractedColorDto>?,
val blurhash: String?, val blurhash: String?,
val crop: CropDto? val crop: CropDto?,
) )
// Color extracted from cover image // Color extracted from cover image
data class ExtractedColorDto( data class ExtractedColorDto(
val swatch: String?, val swatch: String?,
val rgbHex: String? val rgbHex: String?,
) )
// Crop descriptor // Crop descriptor
@@ -28,5 +28,5 @@ data class ExtractedColorDto(
data class CropDto( data class CropDto(
val origin: PointDto?, val origin: PointDto?,
val target: PointDto?, val target: PointDto?,
val circular: Boolean? val circular: Boolean?,
) )

View File

@@ -7,7 +7,7 @@ data class MetadataDto(
val duplicationProtection: Boolean?, val duplicationProtection: Boolean?,
val featuredListMemberships: List<FeaturedListMembershipDto>?, val featuredListMemberships: List<FeaturedListMembershipDto>?,
val lastEdit: LastEditDto?, val lastEdit: LastEditDto?,
val versionMetadata: VersionMetadataDto? val versionMetadata: VersionMetadataDto?,
) )
// Access settings // Access settings
@@ -15,14 +15,14 @@ data class MetadataDto(
data class AccessDto( data class AccessDto(
val groupRead: List<String>?, val groupRead: List<String>?,
val folderGroupIds: List<String>?, val folderGroupIds: List<String>?,
val features: List<String>? val features: List<String>?,
) )
// Featured list membership // Featured list membership
data class FeaturedListMembershipDto( data class FeaturedListMembershipDto(
val list: String?, val list: String?,
val addedAt: Long? val addedAt: Long?,
) )
// Last edit information // Last edit information
@@ -30,7 +30,7 @@ data class FeaturedListMembershipDto(
data class LastEditDto( data class LastEditDto(
val editorUserId: String?, val editorUserId: String?,
val editorUsername: String?, val editorUsername: String?,
val editTimestamp: Long? val editTimestamp: Long?,
) )
// Version metadata // Version metadata
@@ -38,5 +38,5 @@ data class LastEditDto(
data class VersionMetadataDto( data class VersionMetadataDto(
val version: Int?, val version: Int?,
val created: Long?, val created: Long?,
val creator: String? val creator: String?,
) )

View File

@@ -17,15 +17,15 @@ data class QuestionDto(
val questionFormat: Int?, val questionFormat: Int?,
val languageInfo: LanguageInfoDto?, val languageInfo: LanguageInfoDto?,
val media: List<MediaItemDto>?, val media: List<MediaItemDto>?,
val choiceRange: ChoiceRangeDto? val choiceRange: ChoiceRangeDto?,
) )
// Choice option // Choice option
data class ChoiceDto( data class ChoiceDto(
val answer: String?, val answer: String?,
val correct: Boolean?, val correct: Boolean,
val languageInfo: LanguageInfoDto? val languageInfo: LanguageInfoDto?,
) )
// Optional video attachment // Optional video attachment
@@ -35,7 +35,7 @@ data class VideoDto(
val startTime: Int?, val startTime: Int?,
val endTime: Int?, val endTime: Int?,
val service: String?, val service: String?,
val fullUrl: String? val fullUrl: String?,
) )
// Image metadata appearing in multiple places // Image metadata appearing in multiple places
@@ -50,7 +50,7 @@ data class ImageMetadataDto(
val width: Int? = null, val width: Int? = null,
val height: Int? = null, val height: Int? = null,
val effects: List<String>? = null, val effects: List<String>? = null,
val crop: CropDto? = null val crop: CropDto? = null,
) )
// Generic media item on question // Generic media item on question
@@ -67,7 +67,7 @@ data class MediaItemDto(
val resources: String? = null, val resources: String? = null,
val width: Int? = null, val width: Int? = null,
val height: Int? = null, val height: Int? = null,
val crop: CropDto? = null val crop: CropDto? = null,
) )
// Slider range for "slider" question type // Slider range for "slider" question type
@@ -77,5 +77,5 @@ data class ChoiceRangeDto(
val end: Int?, val end: Int?,
val step: Int?, val step: Int?,
val correct: Int?, val correct: Int?,
val tolerance: Int? val tolerance: Int?,
) )

View File

@@ -32,5 +32,5 @@ data class QuizResponseDto(
val hasRestrictedContent: Boolean?, val hasRestrictedContent: Boolean?,
val type: String?, val type: String?,
val created: Long?, val created: Long?,
val modified: Long? val modified: Long?,
) )

View File

@@ -42,8 +42,14 @@
], ],
"blurhash": "UuJ*#Qxtx]xaCAj[W=WqEma}M{R*M|WVn#j?", "blurhash": "UuJ*#Qxtx]xaCAj[W=WqEma}M{R*M|WVn#j?",
"crop": { "crop": {
"origin": {"x": 227, "y": 0}, "origin": {
"target": {"x": 1948, "y": 1299}, "x": 227,
"y": 0
},
"target": {
"x": 1948,
"y": 1299
},
"circular": false "circular": false
} }
}, },
@@ -55,8 +61,24 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "True", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "False", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "answer": "True",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "False",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"layout": "TRUE_FALSE", "layout": "TRUE_FALSE",
"image": "https://media.kahoot.it/b2709905-1c6e-45a0-9cc1-34c6580495e5", "image": "https://media.kahoot.it/b2709905-1c6e-45a0-9cc1-34c6580495e5",
@@ -71,9 +93,18 @@
"height": 1406 "height": 1406
}, },
"resources": "mikroman6/Moment/Getty Images", "resources": "mikroman6/Moment/Getty Images",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -83,15 +114,56 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "A monument to the god Ra", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "A tomb", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "A monument to the god Ra",
{"answer": "A momument to a great war victory", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": false,
{"answer": "A temple", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "A tomb",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "A momument to a great war victory",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "A temple",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"resources": "", "resources": "",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [ "media": [
{ {
"type": "background_image", "type": "background_image",
@@ -105,7 +177,17 @@
"resources": "Nick Brundle Photography/Moment/Getty Images", "resources": "Nick Brundle Photography/Moment/Getty Images",
"width": 2309, "width": 2309,
"height": 1299, "height": 1299,
"crop": {"origin": {"x": 227, "y": 0}, "target": {"x": 1948, "y": 1299}, "circular": false} "crop": {
"origin": {
"x": 227,
"y": 0
},
"target": {
"x": 1948,
"y": 1299
},
"circular": false
}
} }
] ]
}, },
@@ -116,10 +198,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "As a tourist destination", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "A monument to Ninurta, the god of farmers", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "As a tourist destination",
{"answer": "An engagement gift from a king to his future queen", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": false,
{"answer": "A gift for the king's wife", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "A monument to Ninurta, the god of farmers",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "An engagement gift from a king to his future queen",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "A gift for the king's wife",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/7bce7efb-3d94-495c-905f-9c14190b7910", "image": "https://media.kahoot.it/7bce7efb-3d94-495c-905f-9c14190b7910",
"imageMetadata": { "imageMetadata": {
@@ -129,9 +243,18 @@
"effects": [] "effects": []
}, },
"resources": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Hanging_Gardens_of_Babylon.jpg CC0", "resources": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Hanging_Gardens_of_Babylon.jpg CC0",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -141,10 +264,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "Greece", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "Turkey", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "Greece",
{"answer": "Syria", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": false,
{"answer": "Iran", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Turkey",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Syria",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Iran",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/f999f2a2-5450-4821-a3c8-94288720bd46", "image": "https://media.kahoot.it/f999f2a2-5450-4821-a3c8-94288720bd46",
"imageMetadata": { "imageMetadata": {
@@ -154,9 +309,18 @@
"effects": [] "effects": []
}, },
"resources": "Zee Prime at cs.wikipedia [GFDL (http://www.gnu.org/copyleft/fdl.html), CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0/) or CC BY-SA 2.5 (https://creativecommons.org/licenses/by-sa/2.5)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/1/1d/Miniaturk_009.jpg", "resources": "Zee Prime at cs.wikipedia [GFDL (http://www.gnu.org/copyleft/fdl.html), CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0/) or CC BY-SA 2.5 (https://creativecommons.org/licenses/by-sa/2.5)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/1/1d/Miniaturk_009.jpg",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -166,10 +330,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "To become famous", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "It was an accident", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "To become famous",
{"answer": "He was angry at the gods", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": true,
{"answer": "Because of a bet", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "It was an accident",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "He was angry at the gods",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Because of a bet",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/fe2c5c06-6d2e-4a5a-9441-a9c77391130e_opt", "image": "https://media.kahoot.it/fe2c5c06-6d2e-4a5a-9441-a9c77391130e_opt",
"imageMetadata": { "imageMetadata": {
@@ -179,9 +375,19 @@
"effects": [] "effects": []
}, },
"resources": " [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a9/Temple_of_Artemis.jpg", "resources": " [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a9/Temple_of_Artemis.jpg",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"id": "",
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -191,10 +397,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "Sparta", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "Athens", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "Sparta",
{"answer": "Olympia", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": false,
{"answer": "Delphi", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Athens",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Olympia",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Delphi",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/9074a275-1874-4cb9-9c9f-248173ceae9d", "image": "https://media.kahoot.it/9074a275-1874-4cb9-9c9f-248173ceae9d",
"imageMetadata": { "imageMetadata": {
@@ -202,12 +440,31 @@
"contentType": "image/*", "contentType": "image/*",
"resources": " [Public domain or Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/6/66/Le_Jupiter_Olympien_ou_l%27art_de_la_sculpture_antique.jpg", "resources": " [Public domain or Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/6/66/Le_Jupiter_Olympien_ou_l%27art_de_la_sculpture_antique.jpg",
"effects": [], "effects": [],
"crop": {"origin": {"x": 53, "y": 0}, "target": {"x": 577, "y": 866}, "circular": false} "crop": {
"origin": {
"x": 53,
"y": 0
},
"target": {
"x": 577,
"y": 866
},
"circular": false
}
}, },
"resources": " [Public domain or Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/6/66/Le_Jupiter_Olympien_ou_l%27art_de_la_sculpture_antique.jpg", "resources": " [Public domain or Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/6/66/Le_Jupiter_Olympien_ou_l%27art_de_la_sculpture_antique.jpg",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -217,10 +474,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "Darius", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "Xerxes", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "Darius",
{"answer": "Cyrus", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": false,
{"answer": "Mausoleus", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Xerxes",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Cyrus",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Mausoleus",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/38f43ef3-4507-4f11-ae33-f3e833a47d19", "image": "https://media.kahoot.it/38f43ef3-4507-4f11-ae33-f3e833a47d19",
"imageMetadata": { "imageMetadata": {
@@ -234,9 +523,18 @@
"height": 1414 "height": 1414
}, },
"resources": "MirageC/Moment/Getty Images", "resources": "MirageC/Moment/Getty Images",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -245,8 +543,24 @@
"time": 60000, "time": 60000,
"pointsMultiplier": 2, "pointsMultiplier": 2,
"choices": [ "choices": [
{"answer": "Helios", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "helios", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "answer": "Helios",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "helios",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/d4ccbf4e-1026-46ad-ab35-84dc17c4d3a0_opt", "image": "https://media.kahoot.it/d4ccbf4e-1026-46ad-ab35-84dc17c4d3a0_opt",
"imageMetadata": { "imageMetadata": {
@@ -254,12 +568,32 @@
"contentType": "image/*", "contentType": "image/*",
"resources": "By gravure sur bois de Sidney Barclay numérisée Google [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/5/5f/Colosse_de_Rhodes_%28Barclay%29.jpg", "resources": "By gravure sur bois de Sidney Barclay numérisée Google [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/5/5f/Colosse_de_Rhodes_%28Barclay%29.jpg",
"effects": [], "effects": [],
"crop": {"origin": {"x": 49, "y": 83}, "target": {"x": 531, "y": 796}, "circular": false} "crop": {
"origin": {
"x": 49,
"y": 83
},
"target": {
"x": 531,
"y": 796
},
"circular": false
}
}, },
"resources": "By gravure sur bois de Sidney Barclay numérisée Google [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/5/5f/Colosse_de_Rhodes_%28Barclay%29.jpg", "resources": "By gravure sur bois de Sidney Barclay numérisée Google [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/5/5f/Colosse_de_Rhodes_%28Barclay%29.jpg",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"id": "",
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -269,10 +603,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "Fire", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "Earthquake", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "Fire",
{"answer": "Tidal Wave", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": false,
{"answer": "Storm", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Earthquake",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Tidal Wave",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "Storm",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/e2d22765-942b-4dbd-9fd6-d71142d775c3", "image": "https://media.kahoot.it/e2d22765-942b-4dbd-9fd6-d71142d775c3",
"imageMetadata": { "imageMetadata": {
@@ -280,12 +646,31 @@
"contentType": "image/*", "contentType": "image/*",
"resources": "Emad Victor SHENOUDA [Attribution], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/3/33/PHAROS2013-3000x2250.jpg", "resources": "Emad Victor SHENOUDA [Attribution], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/3/33/PHAROS2013-3000x2250.jpg",
"effects": [], "effects": [],
"crop": {"origin": {"x": 0, "y": 10}, "target": {"x": 1024, "y": 683}, "circular": false} "crop": {
"origin": {
"x": 0,
"y": 10
},
"target": {
"x": 1024,
"y": 683
},
"circular": false
}
}, },
"resources": "Emad Victor SHENOUDA [Attribution], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/3/33/PHAROS2013-3000x2250.jpg", "resources": "Emad Victor SHENOUDA [Attribution], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/3/33/PHAROS2013-3000x2250.jpg",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -295,10 +680,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "The Colossus of Rhodes", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "The Lighthouse of Alexandria", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "The Colossus of Rhodes",
{"answer": "The Mausoleum at Halicarnassus", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": false,
{"answer": "The Great Pyramid of Giza", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "The Lighthouse of Alexandria",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "The Mausoleum at Halicarnassus",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "The Great Pyramid of Giza",
"correct": true,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/19382163-196f-495d-9a84-c2d8c3fd716c", "image": "https://media.kahoot.it/19382163-196f-495d-9a84-c2d8c3fd716c",
"imageMetadata": { "imageMetadata": {
@@ -306,12 +723,32 @@
"contentType": "image/*", "contentType": "image/*",
"resources": "By The original uploader was Mark22 at English Wikipedia (Transferred from en.wikipedia to Commons.) [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/b/b7/SevenWondersOfTheWorld.png", "resources": "By The original uploader was Mark22 at English Wikipedia (Transferred from en.wikipedia to Commons.) [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/b/b7/SevenWondersOfTheWorld.png",
"effects": [], "effects": [],
"crop": {"origin": {"x": 19, "y": 0}, "target": {"x": 491, "y": 736}, "circular": false} "crop": {
"origin": {
"x": 19,
"y": 0
},
"target": {
"x": 491,
"y": 736
},
"circular": false
}
}, },
"resources": "By The original uploader was Mark22 at English Wikipedia (Transferred from en.wikipedia to Commons.) [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/b/b7/SevenWondersOfTheWorld.png", "resources": "By The original uploader was Mark22 at English Wikipedia (Transferred from en.wikipedia to Commons.) [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/b/b7/SevenWondersOfTheWorld.png",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"id": "",
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -319,7 +756,13 @@
"question": "How many of the Seven Wonders still exist?", "question": "How many of the Seven Wonders still exist?",
"time": 20000, "time": 20000,
"pointsMultiplier": 2, "pointsMultiplier": 2,
"choiceRange": {"start": 0, "end": 7, "step": 1, "correct": 1, "tolerance": 0}, "choiceRange": {
"start": 0,
"end": 7,
"step": 1,
"correct": 1,
"tolerance": 0
},
"image": "https://media.kahoot.it/b431b3aa-4a46-49c9-b4ac-aa1dde40333f", "image": "https://media.kahoot.it/b431b3aa-4a46-49c9-b4ac-aa1dde40333f",
"imageMetadata": { "imageMetadata": {
"id": "b431b3aa-4a46-49c9-b4ac-aa1dde40333f", "id": "b431b3aa-4a46-49c9-b4ac-aa1dde40333f",
@@ -328,9 +771,19 @@
"effects": [] "effects": []
}, },
"resources": "By Kandi [GFDL (http://www.gnu.org/copyleft/fdl.html) or CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a4/Seven_Wonders_of_the_Ancient_World.png", "resources": "By Kandi [GFDL (http://www.gnu.org/copyleft/fdl.html) or CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a4/Seven_Wonders_of_the_Ancient_World.png",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"id": "",
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
}, },
{ {
@@ -340,10 +793,42 @@
"points": true, "points": true,
"pointsMultiplier": 1, "pointsMultiplier": 1,
"choices": [ "choices": [
{"answer": "The Great Pyramid of Giza", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, {
{"answer": "The Temple of Artemis", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "answer": "The Great Pyramid of Giza",
{"answer": "The Mausoleum at Halicarnassus", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}, "correct": true,
{"answer": "The Colossus of Rhodes", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}} "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "The Temple of Artemis",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "The Mausoleum at Halicarnassus",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
},
{
"answer": "The Colossus of Rhodes",
"correct": false,
"languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
}
}
], ],
"image": "https://media.kahoot.it/34b01038-031c-4d23-b8a0-55402916586f_opt", "image": "https://media.kahoot.it/34b01038-031c-4d23-b8a0-55402916586f_opt",
"imageMetadata": { "imageMetadata": {
@@ -351,16 +836,39 @@
"contentType": "image/*", "contentType": "image/*",
"resources": "By Varios [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/d/d6/Siete_maravillas_antiguas.jpg", "resources": "By Varios [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/d/d6/Siete_maravillas_antiguas.jpg",
"effects": [], "effects": [],
"crop": {"origin": {"x": 19, "y": 0}, "target": {"x": 491, "y": 736}, "circular": false} "crop": {
"origin": {
"x": 19,
"y": 0
},
"target": {
"x": 491,
"y": 736
},
"circular": false
}
}, },
"resources": "By Varios [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/d/d6/Siete_maravillas_antiguas.jpg", "resources": "By Varios [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/d/d6/Siete_maravillas_antiguas.jpg",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, "video": {
"id": "",
"startTime": 0,
"endTime": 0,
"service": "youtube",
"fullUrl": ""
},
"questionFormat": 0, "questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"media": [] "media": []
} }
], ],
"contentTags": {"curriculumCodes": [], "generatedCurriculumCodes": []}, "contentTags": {
"curriculumCodes": [],
"generatedCurriculumCodes": []
},
"metadata": { "metadata": {
"access": { "access": {
"groupRead": [ "groupRead": [
@@ -372,12 +880,20 @@
"36022fd9-43e1-4b36-9c98-a6a3b2b53038" "36022fd9-43e1-4b36-9c98-a6a3b2b53038"
], ],
"folderGroupIds": [], "folderGroupIds": [],
"features": ["PremiumEduContent"] "features": [
"PremiumEduContent"
]
}, },
"duplicationProtection": true, "duplicationProtection": true,
"featuredListMemberships": [ "featuredListMemberships": [
{"list": "youngfeatured", "addedAt": 1682336780289}, {
{"list": "featured", "addedAt": 1682336738189} "list": "youngfeatured",
"addedAt": 1682336780289
},
{
"list": "featured",
"addedAt": 1682336738189
}
], ],
"lastEdit": { "lastEdit": {
"editorUserId": "4c1574ee-de54-40a2-be15-8d72b333afad", "editorUserId": "4c1574ee-de54-40a2-be15-8d72b333afad",
@@ -392,9 +908,17 @@
}, },
"resources": "Nick Brundle Photography/Moment/Getty Images", "resources": "Nick Brundle Photography/Moment/Getty Images",
"slug": "seven-wonders-of-the-ancient-world", "slug": "seven-wonders-of-the-ancient-world",
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, "languageInfo": {
"language": "en-US",
"lastUpdatedOn": 1741920189202,
"readAloudSupported": true
},
"inventoryItemIds": [], "inventoryItemIds": [],
"channels": [{"id": "247c3eb4-af80-4c1f-b006-558682c7bd2f"}], "channels": [
{
"id": "247c3eb4-af80-4c1f-b006-558682c7bd2f"
}
],
"isValid": true, "isValid": true,
"playAsGuest": true, "playAsGuest": true,
"hasRestrictedContent": false, "hasRestrictedContent": false,

View File

@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class Access( data class Access(
val groupRead: List<String>?, val groupRead: List<String>?,
val folderGroupIds: List<String>?, val folderGroupIds: List<String>?,
val features: List<String>? val features: List<String>?,
) )

View File

@@ -2,6 +2,6 @@ package dev.adriankuta.kahootquiz.domain.models
data class Choice( data class Choice(
val answer: String?, val answer: String?,
val correct: Boolean?, val correct: Boolean,
val languageInfo: LanguageInfo? val languageInfo: LanguageInfo? = null,
) )

View File

@@ -7,5 +7,5 @@ data class ChoiceRange(
val end: Int?, val end: Int?,
val step: Int?, val step: Int?,
val correct: Int?, val correct: Int?,
val tolerance: Int? val tolerance: Int?,
) )

View File

@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class ContentTags( data class ContentTags(
val curriculumCodes: List<String>?, val curriculumCodes: List<String>?,
val generatedCurriculumCodes: List<String>? val generatedCurriculumCodes: List<String>?,
) )

View File

@@ -13,5 +13,5 @@ data class CoverMetadata(
val height: Int?, val height: Int?,
val extractedColors: List<ExtractedColor>?, val extractedColors: List<ExtractedColor>?,
val blurhash: String?, val blurhash: String?,
val crop: Crop? val crop: Crop?,
) )

View File

@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class Crop( data class Crop(
val origin: Point?, val origin: Point?,
val target: Point?, val target: Point?,
val circular: Boolean? val circular: Boolean?,
) )

View File

@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class ExtractedColor( data class ExtractedColor(
val swatch: String?, val swatch: String?,
val rgbHex: String? val rgbHex: String?,
) )

View File

@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class FeaturedListMembership( data class FeaturedListMembership(
val list: String?, val list: String?,
val addedAt: Long? val addedAt: Long?,
) )

View File

@@ -12,5 +12,5 @@ data class ImageMetadata(
val width: Int? = null, val width: Int? = null,
val height: Int? = null, val height: Int? = null,
val effects: List<String>? = null, val effects: List<String>? = null,
val crop: Crop? = null val crop: Crop? = null,
) )

View File

@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class LanguageInfo( data class LanguageInfo(
val language: String?, val language: String?,
val lastUpdatedOn: Long?, val lastUpdatedOn: Long?,
val readAloudSupported: Boolean? val readAloudSupported: Boolean?,
) )

View File

@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class LastEdit( data class LastEdit(
val editorUserId: String?, val editorUserId: String?,
val editorUsername: String?, val editorUsername: String?,
val editTimestamp: Long? val editTimestamp: Long?,
) )

View File

@@ -14,5 +14,5 @@ data class MediaItem(
val resources: String? = null, val resources: String? = null,
val width: Int? = null, val width: Int? = null,
val height: Int? = null, val height: Int? = null,
val crop: Crop? = null val crop: Crop? = null,
) )

View File

@@ -7,5 +7,5 @@ data class Metadata(
val duplicationProtection: Boolean?, val duplicationProtection: Boolean?,
val featuredListMemberships: List<FeaturedListMembership>?, val featuredListMemberships: List<FeaturedListMembership>?,
val lastEdit: LastEdit?, val lastEdit: LastEdit?,
val versionMetadata: VersionMetadata? val versionMetadata: VersionMetadata?,
) )

View File

@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class Point( data class Point(
val x: Int?, val x: Int?,
val y: Int? val y: Int?,
) )

View File

@@ -8,16 +8,16 @@ data class Question(
val type: String?, val type: String?,
val question: String?, val question: String?,
val time: Duration?, val time: Duration?,
val points: Boolean?, val points: Boolean? = null,
val pointsMultiplier: Int?, val pointsMultiplier: Int?,
val choices: List<Choice>?, val choices: List<Choice>?,
val layout: String?, val layout: String? = null,
val image: String?, val image: String? = null,
val imageMetadata: ImageMetadata?, val imageMetadata: ImageMetadata?,
val resources: String?, val resources: String? = null,
val video: Video?, val video: Video? = null,
val questionFormat: Int?, val questionFormat: Int? = null,
val languageInfo: LanguageInfo?, val languageInfo: LanguageInfo? = null,
val media: List<MediaItem>?, val media: List<MediaItem>? = null,
val choiceRange: ChoiceRange? val choiceRange: ChoiceRange? = null,
) )

View File

@@ -31,5 +31,5 @@ data class Quiz(
val hasRestrictedContent: Boolean?, val hasRestrictedContent: Boolean?,
val type: String?, val type: String?,
val created: Long?, val created: Long?,
val modified: Long? val modified: Long?,
) )

View File

@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
data class VersionMetadata( data class VersionMetadata(
val version: Int?, val version: Int?,
val created: Long?, val created: Long?,
val creator: String? val creator: String?,
) )

View File

@@ -5,5 +5,5 @@ data class Video(
val startTime: Int?, val startTime: Int?,
val endTime: Int?, val endTime: Int?,
val service: String?, val service: String?,
val fullUrl: String? val fullUrl: String?,
) )

View File

@@ -5,7 +5,7 @@ import dev.adriankuta.kahootquiz.domain.repositories.QuizRepository
import javax.inject.Inject import javax.inject.Inject
class GetQuizUseCase @Inject constructor( class GetQuizUseCase @Inject constructor(
private val quizRepository: QuizRepository private val quizRepository: QuizRepository,
) { ) {
suspend operator fun invoke(): Quiz { suspend operator fun invoke(): Quiz {

View File

@@ -1,5 +1,4 @@
[versions] [versions]
retrofit = "3.0.0"
targetSdk = "36" targetSdk = "36"
compileSdk = "36" compileSdk = "36"
minSdk = "23" minSdk = "23"
@@ -21,6 +20,8 @@ animation = "1.9.0"
appUpdateKtx = "2.1.0" appUpdateKtx = "2.1.0"
appcompat = "1.7.1" appcompat = "1.7.1"
billing = "8.0.0" billing = "8.0.0"
coilCompose = "3.3.0"
coilNetworkOkhttp = "3.3.0"
coreTest = "1.7.0" # https://developer.android.com/jetpack/androidx/releases/test coreTest = "1.7.0" # https://developer.android.com/jetpack/androidx/releases/test
datastorePreferences = "1.1.7" # https://developer.android.com/topic/libraries/architecture/datastore#preferences-datastore-dependencies datastorePreferences = "1.1.7" # https://developer.android.com/topic/libraries/architecture/datastore#preferences-datastore-dependencies
datetime = "0.7.1" # https://github.com/Kotlin/kotlinx-datetime/releases datetime = "0.7.1" # https://github.com/Kotlin/kotlinx-datetime/releases
@@ -46,6 +47,7 @@ material = "1.12.0"
materialIconsExtended = "1.7.8" materialIconsExtended = "1.7.8"
mockk = "1.14.5" # https://github.com/mockk/mockk/releases mockk = "1.14.5" # https://github.com/mockk/mockk/releases
playServicesAds = "24.5.0" playServicesAds = "24.5.0"
retrofit = "3.0.0"
reviewKtx = "2.0.2" reviewKtx = "2.0.2"
room = "2.7.2" room = "2.7.2"
secrets = "2.0.1" secrets = "2.0.1"
@@ -94,6 +96,8 @@ androidx-work-testing = { group = "androidx.work", name = "work-testing", versio
app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
billing-ktx = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" } billing-ktx = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilNetworkOkhttp" }
detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" }
detekt-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
dotlottie-android = { module = "com.github.LottieFiles:dotlottie-android", version.ref = "dotlottieAndroid" } dotlottie-android = { module = "com.github.LottieFiles:dotlottie-android", version.ref = "dotlottieAndroid" }

View File

@@ -7,7 +7,7 @@ import dev.adriankuta.kahootquiz.model.data.mappers.toDomainModel
import javax.inject.Inject import javax.inject.Inject
internal class QuizRepositoryImpl @Inject constructor( internal class QuizRepositoryImpl @Inject constructor(
private val quizApi: QuizApi private val quizApi: QuizApi,
) : QuizRepository { ) : QuizRepository {
override suspend fun getQuiz(): Quiz { override suspend fun getQuiz(): Quiz {

View File

@@ -15,6 +15,6 @@ internal abstract class RepositoryModule {
@Binds @Binds
@Singleton @Singleton
abstract fun bindsQuizRepository( abstract fun bindsQuizRepository(
quizRepositoryImpl: QuizRepositoryImpl quizRepositoryImpl: QuizRepositoryImpl,
): QuizRepository ): QuizRepository
} }

View File

@@ -1,7 +1,44 @@
package dev.adriankuta.kahootquiz.model.data.mappers package dev.adriankuta.kahootquiz.model.data.mappers
import dev.adriankuta.kahootquiz.core.network.models.* import dev.adriankuta.kahootquiz.core.network.models.AccessDto
import dev.adriankuta.kahootquiz.domain.models.* import dev.adriankuta.kahootquiz.core.network.models.ChannelDto
import dev.adriankuta.kahootquiz.core.network.models.ChoiceDto
import dev.adriankuta.kahootquiz.core.network.models.ChoiceRangeDto
import dev.adriankuta.kahootquiz.core.network.models.ContentTagsDto
import dev.adriankuta.kahootquiz.core.network.models.CoverMetadataDto
import dev.adriankuta.kahootquiz.core.network.models.CropDto
import dev.adriankuta.kahootquiz.core.network.models.ExtractedColorDto
import dev.adriankuta.kahootquiz.core.network.models.FeaturedListMembershipDto
import dev.adriankuta.kahootquiz.core.network.models.ImageMetadataDto
import dev.adriankuta.kahootquiz.core.network.models.LanguageInfoDto
import dev.adriankuta.kahootquiz.core.network.models.LastEditDto
import dev.adriankuta.kahootquiz.core.network.models.MediaItemDto
import dev.adriankuta.kahootquiz.core.network.models.MetadataDto
import dev.adriankuta.kahootquiz.core.network.models.PointDto
import dev.adriankuta.kahootquiz.core.network.models.QuestionDto
import dev.adriankuta.kahootquiz.core.network.models.QuizResponseDto
import dev.adriankuta.kahootquiz.core.network.models.VersionMetadataDto
import dev.adriankuta.kahootquiz.core.network.models.VideoDto
import dev.adriankuta.kahootquiz.domain.models.Access
import dev.adriankuta.kahootquiz.domain.models.Channel
import dev.adriankuta.kahootquiz.domain.models.Choice
import dev.adriankuta.kahootquiz.domain.models.ChoiceRange
import dev.adriankuta.kahootquiz.domain.models.ContentTags
import dev.adriankuta.kahootquiz.domain.models.CoverMetadata
import dev.adriankuta.kahootquiz.domain.models.Crop
import dev.adriankuta.kahootquiz.domain.models.ExtractedColor
import dev.adriankuta.kahootquiz.domain.models.FeaturedListMembership
import dev.adriankuta.kahootquiz.domain.models.ImageMetadata
import dev.adriankuta.kahootquiz.domain.models.LanguageInfo
import dev.adriankuta.kahootquiz.domain.models.LastEdit
import dev.adriankuta.kahootquiz.domain.models.MediaItem
import dev.adriankuta.kahootquiz.domain.models.Metadata
import dev.adriankuta.kahootquiz.domain.models.Point
import dev.adriankuta.kahootquiz.domain.models.Question
import dev.adriankuta.kahootquiz.domain.models.Quiz
import dev.adriankuta.kahootquiz.domain.models.QuizId
import dev.adriankuta.kahootquiz.domain.models.VersionMetadata
import dev.adriankuta.kahootquiz.domain.models.Video
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
internal fun QuizResponseDto.toDomainModel(): Quiz = internal fun QuizResponseDto.toDomainModel(): Quiz =
@@ -34,7 +71,7 @@ internal fun QuizResponseDto.toDomainModel(): Quiz =
hasRestrictedContent = hasRestrictedContent, hasRestrictedContent = hasRestrictedContent,
type = type, type = type,
created = created, created = created,
modified = modified modified = modified,
) )
private fun CoverMetadataDto.toDomain(): CoverMetadata = CoverMetadata( private fun CoverMetadataDto.toDomain(): CoverMetadata = CoverMetadata(
@@ -48,25 +85,25 @@ private fun CoverMetadataDto.toDomain(): CoverMetadata = CoverMetadata(
height = height, height = height,
extractedColors = extractedColors?.map { it.toDomain() }, extractedColors = extractedColors?.map { it.toDomain() },
blurhash = blurhash, blurhash = blurhash,
crop = crop?.toDomain() crop = crop?.toDomain(),
) )
private fun ExtractedColorDto.toDomain(): ExtractedColor = ExtractedColor( private fun ExtractedColorDto.toDomain(): ExtractedColor = ExtractedColor(
swatch = swatch, swatch = swatch,
rgbHex = rgbHex rgbHex = rgbHex,
) )
private fun CropDto.toDomain(): Crop = Crop( private fun CropDto.toDomain(): Crop = Crop(
origin = origin?.toDomain(), origin = origin?.toDomain(),
target = target?.toDomain(), target = target?.toDomain(),
circular = circular circular = circular,
) )
private fun PointDto.toDomain(): Point = Point(x = x, y = y) private fun PointDto.toDomain(): Point = Point(x = x, y = y)
private fun ContentTagsDto.toDomain(): ContentTags = ContentTags( private fun ContentTagsDto.toDomain(): ContentTags = ContentTags(
curriculumCodes = curriculumCodes, curriculumCodes = curriculumCodes,
generatedCurriculumCodes = generatedCurriculumCodes generatedCurriculumCodes = generatedCurriculumCodes,
) )
private fun MetadataDto.toDomain(): Metadata = Metadata( private fun MetadataDto.toDomain(): Metadata = Metadata(
@@ -74,36 +111,36 @@ private fun MetadataDto.toDomain(): Metadata = Metadata(
duplicationProtection = duplicationProtection, duplicationProtection = duplicationProtection,
featuredListMemberships = featuredListMemberships?.map { it.toDomain() }, featuredListMemberships = featuredListMemberships?.map { it.toDomain() },
lastEdit = lastEdit?.toDomain(), lastEdit = lastEdit?.toDomain(),
versionMetadata = versionMetadata?.toDomain() versionMetadata = versionMetadata?.toDomain(),
) )
private fun AccessDto.toDomain(): Access = Access( private fun AccessDto.toDomain(): Access = Access(
groupRead = groupRead, groupRead = groupRead,
folderGroupIds = folderGroupIds, folderGroupIds = folderGroupIds,
features = features features = features,
) )
private fun FeaturedListMembershipDto.toDomain(): FeaturedListMembership = FeaturedListMembership( private fun FeaturedListMembershipDto.toDomain(): FeaturedListMembership = FeaturedListMembership(
list = list, list = list,
addedAt = addedAt addedAt = addedAt,
) )
private fun LastEditDto.toDomain(): LastEdit = LastEdit( private fun LastEditDto.toDomain(): LastEdit = LastEdit(
editorUserId = editorUserId, editorUserId = editorUserId,
editorUsername = editorUsername, editorUsername = editorUsername,
editTimestamp = editTimestamp editTimestamp = editTimestamp,
) )
private fun VersionMetadataDto.toDomain(): VersionMetadata = VersionMetadata( private fun VersionMetadataDto.toDomain(): VersionMetadata = VersionMetadata(
version = version, version = version,
created = created, created = created,
creator = creator creator = creator,
) )
private fun LanguageInfoDto.toDomain(): LanguageInfo = LanguageInfo( private fun LanguageInfoDto.toDomain(): LanguageInfo = LanguageInfo(
language = language, language = language,
lastUpdatedOn = lastUpdatedOn, lastUpdatedOn = lastUpdatedOn,
readAloudSupported = readAloudSupported readAloudSupported = readAloudSupported,
) )
private fun ChannelDto.toDomain(): Channel = Channel(id = id) private fun ChannelDto.toDomain(): Channel = Channel(id = id)
@@ -123,13 +160,13 @@ private fun QuestionDto.toDomain(): Question = Question(
questionFormat = questionFormat, questionFormat = questionFormat,
languageInfo = languageInfo?.toDomain(), languageInfo = languageInfo?.toDomain(),
media = media?.map { it.toDomain() }, media = media?.map { it.toDomain() },
choiceRange = choiceRange?.toDomain() choiceRange = choiceRange?.toDomain(),
) )
private fun ChoiceDto.toDomain(): Choice = Choice( private fun ChoiceDto.toDomain(): Choice = Choice(
answer = answer, answer = answer,
correct = correct, correct = correct,
languageInfo = languageInfo?.toDomain() languageInfo = languageInfo?.toDomain(),
) )
private fun VideoDto.toDomain(): Video = Video( private fun VideoDto.toDomain(): Video = Video(
@@ -137,7 +174,7 @@ private fun VideoDto.toDomain(): Video = Video(
startTime = startTime, startTime = startTime,
endTime = endTime, endTime = endTime,
service = service, service = service,
fullUrl = fullUrl fullUrl = fullUrl,
) )
private fun ImageMetadataDto.toDomain(): ImageMetadata = ImageMetadata( private fun ImageMetadataDto.toDomain(): ImageMetadata = ImageMetadata(
@@ -150,7 +187,7 @@ private fun ImageMetadataDto.toDomain(): ImageMetadata = ImageMetadata(
width = width, width = width,
height = height, height = height,
effects = effects, effects = effects,
crop = crop?.toDomain() crop = crop?.toDomain(),
) )
private fun MediaItemDto.toDomain(): MediaItem = MediaItem( private fun MediaItemDto.toDomain(): MediaItem = MediaItem(
@@ -165,7 +202,7 @@ private fun MediaItemDto.toDomain(): MediaItem = MediaItem(
resources = resources, resources = resources,
width = width, width = width,
height = height, height = height,
crop = crop?.toDomain() crop = crop?.toDomain(),
) )
private fun ChoiceRangeDto.toDomain(): ChoiceRange = ChoiceRange( private fun ChoiceRangeDto.toDomain(): ChoiceRange = ChoiceRange(
@@ -173,5 +210,5 @@ private fun ChoiceRangeDto.toDomain(): ChoiceRange = ChoiceRange(
end = end, end = end,
step = step, step = step,
correct = correct, correct = correct,
tolerance = tolerance tolerance = tolerance,
) )

View File

@@ -25,6 +25,7 @@ rootProject.name = "KahootQuiz"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include(":app") include(":app")
include(":core:designsystem")
include(":core:network") include(":core:network")
include(":domain") include(":domain")
include(":model:data") include(":model:data")

View File

@@ -9,7 +9,11 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.designsystem)
implementation(projects.domain) implementation(projects.domain)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.timber) implementation(libs.timber)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
} }

View File

@@ -1,33 +1,380 @@
package dev.adriankuta.kahootquiz.ui.quiz package dev.adriankuta.kahootquiz.ui.quiz
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.Column
import androidx.compose.foundation.layout.PaddingValues
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.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.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
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.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.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.Question
import kotlin.time.Duration.Companion.seconds
import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR
@Composable @Composable
fun QuizScreen( fun QuizScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: QuizScreenViewModel = hiltViewModel() viewModel: QuizScreenViewModel = hiltViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
QuizScreen( QuizScreen(
uiState = uiState, uiState = uiState,
modifier = modifier onSelect = viewModel::onChoiceSelected,
modifier = modifier.fillMaxSize(),
) )
} }
@Composable @Composable
private fun QuizScreen( private fun QuizScreen(
uiState: QuizUiState, uiState: QuizUiState,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier) { Box(modifier.fillMaxSize()) {
Text(uiState.quiz?.id?.value ?: "") Image(
painter = painterResource(id = DesignR.drawable.bg_image),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Toolbar(
modifier = Modifier
.fillMaxWidth()
.height(72.dp)
.padding(8.dp),
)
QuestionContent(
question = uiState.currentQuestion ?: return,
modifier = Modifier.padding(horizontal = 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 Toolbar(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier,
) {
Text(
text = "2/24",
modifier = Modifier
.align(Alignment.CenterStart)
.background(
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),
)
.padding(horizontal = 8.dp, vertical = 16.dp),
)
}
}
@Composable
private fun Choices(
choices: List<Choice>,
onSelect: (Int) -> Unit,
answer: AnswerUiState?,
modifier: Modifier = Modifier,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier,
) {
itemsIndexed(choices) { index, choice ->
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
@Composable
private fun QuizScreenPreview() {
KahootQuizTheme {
val sampleQuestion = Question(
type = "quiz",
image = "", // Add a sample image URL or leave empty
question = "What is the capital of France?",
choices = listOf(
Choice(answer = "Berlin", correct = false),
Choice(answer = "Madrid", correct = false),
Choice(answer = "Paris", correct = true),
Choice(answer = "Rome", correct = false),
),
pointsMultiplier = 1,
time = 30.seconds,
questionFormat = 0,
imageMetadata = null,
)
QuizScreen(
uiState = QuizUiState(
currentQuestion = sampleQuestion,
),
onSelect = {},
)
}
}
@Preview
@Composable
private fun QuizScreenRevealedAnswerPreview() {
KahootQuizTheme {
val sampleQuestion = Question(
type = "quiz",
image = "", // Add a sample image URL or leave empty
question = "What is the capital of France?",
choices = listOf(
Choice(answer = "Berlin", correct = false),
Choice(answer = "Madrid", correct = false),
Choice(answer = "Paris", correct = true),
Choice(answer = "Rome", correct = false),
),
pointsMultiplier = 1,
time = 30.seconds,
questionFormat = 0,
imageMetadata = null,
)
QuizScreen(
uiState = QuizUiState(
currentQuestion = sampleQuestion,
answer = AnswerUiState(
selectedChoiceIndex = 1,
),
),
onSelect = {},
)
} }
} }

View File

@@ -3,29 +3,45 @@ package dev.adriankuta.kahootquiz.ui.quiz
import androidx.lifecycle.ViewModel 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.Quiz import dev.adriankuta.kahootquiz.domain.models.Question
import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
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 _uiState = MutableStateFlow(QuizUiState()) private val _selectedChoiceIndex = MutableStateFlow<Int?>(null)
val uiState: StateFlow<QuizUiState> = _uiState.asStateFlow() val uiState: StateFlow<QuizUiState> = combine(
suspend { getQuizUseCase() }.asFlow(),
_selectedChoiceIndex,
) { quiz, selectedChoiceIndex ->
QuizUiState(
currentQuestion = quiz.questions.first(),
answer = selectedChoiceIndex?.let { AnswerUiState(it) },
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = QuizUiState(),
)
init { fun onChoiceSelected(index: Int) {
viewModelScope.launch { _selectedChoiceIndex.value = index
_uiState.value = QuizUiState(getQuizUseCase())
}
} }
} }
data class QuizUiState( data class QuizUiState(
val quiz: Quiz? = null val currentQuestion: Question? = null,
) val answer: AnswerUiState? = null,
)
data class AnswerUiState(
val selectedChoiceIndex: Int,
)

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="quiz">Quiz</string>
</resources>