mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-14 17:24:21 +02:00
Compare commits
19 Commits
57313de1d7
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
75d1ce86eb | ||
![]() |
d6e77be660 | ||
![]() |
b454701566 | ||
![]() |
34b026ec94 | ||
![]() |
1b57800641 | ||
![]() |
21ba338d38 | ||
![]() |
59218cc2e1 | ||
![]() |
77a3dd9eeb | ||
![]() |
99f1c49713 | ||
![]() |
12638f33d8 | ||
![]() |
3194d2a813 | ||
![]() |
7cd3394098 | ||
![]() |
7d38facda5 | ||
![]() |
d2fce7e7b9 | ||
![]() |
41fd729271 | ||
![]() |
f0bd963d2d | ||
![]() |
710dedb0cc | ||
![]() |
7568abb775 | ||
![]() |
45550ecf76 |
4
.editorconfig
Executable file
4
.editorconfig
Executable file
@@ -0,0 +1,4 @@
|
||||
[*.{kt,kts}]
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
|
4
.idea/gradle.xml
generated
4
.idea/gradle.xml
generated
@@ -26,10 +26,10 @@
|
||||
<option value="$PROJECT_DIR$/build-logic" />
|
||||
<option value="$PROJECT_DIR$/build-logic/convention" />
|
||||
<option value="$PROJECT_DIR$/core" />
|
||||
<option value="$PROJECT_DIR$/core/designsystem" />
|
||||
<option value="$PROJECT_DIR$/core/network" />
|
||||
<option value="$PROJECT_DIR$/data" />
|
||||
<option value="$PROJECT_DIR$/domain" />
|
||||
<option value="$PROJECT_DIR$/model" />
|
||||
<option value="$PROJECT_DIR$/model/data" />
|
||||
<option value="$PROJECT_DIR$/ui" />
|
||||
<option value="$PROJECT_DIR$/ui/quiz" />
|
||||
</set>
|
||||
|
97
README.md
Normal file
97
README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# KahootQuiz — Interview Challenge
|
||||
|
||||
This project is an implementation for an interview-style challenge. It demonstrates a clean, modular
|
||||
Android architecture with a focus on separation of concerns, convention plugins for Gradle, and
|
||||
pragmatic Kotlin usage.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Only image media is supported right now.
|
||||
- Slider question type is not supported.
|
||||
- There is no end/completion screen yet.
|
||||
- Errors in ViewModels are caught but not yet handled (no user-facing error states/actions).
|
||||
|
||||
## Project Overview
|
||||
|
||||
- Multi-module, clean architecture:
|
||||
- `core/` — common utilities (e.g., networking).
|
||||
- `domain/` — pure domain models and repository abstractions, domain models.
|
||||
- `data/` — repository implementations, mappers.
|
||||
- `ui/` — feature UI modules (e.g., `ui/quiz`).
|
||||
- Convention plugins are used to centralize and reuse Gradle configuration across modules (see
|
||||
`build-logic/`).
|
||||
- Kotlin-first approach using language features to keep code concise and readable.
|
||||
|
||||
## How to Build & Run
|
||||
|
||||
1. Requirements:
|
||||
- Android Studio
|
||||
- JDK 21
|
||||
- Gradle wrapper included
|
||||
2. Steps:
|
||||
- Open the project in Android Studio.
|
||||
- Sync Gradle.
|
||||
- Run the `app` configuration on a device/emulator.
|
||||
|
||||
If you prefer the command line: `./gradlew assembleDebug` and then install the generated APK.
|
||||
|
||||
## Architecture Details
|
||||
|
||||
- Data flow follows a standard clean pattern:
|
||||
- `domain.repositories.QuizRepository` defines the contract.
|
||||
- `data.QuizRepositoryImpl` uses `core.network.retrofit.QuizApi` + mappers to produce
|
||||
`domain.models.Quiz`.
|
||||
- UI consumes domain via ViewModels and exposes a `UiState`.
|
||||
- The code emphasizes separation of concerns and testability.
|
||||
|
||||
## Current Limitations & Known Issues
|
||||
|
||||
- Media support:
|
||||
- Only `image` media is supported in the quiz content.
|
||||
- Other media types are not supported.
|
||||
- Question types:
|
||||
- Slider answers are not supported yet.
|
||||
- UX flow:
|
||||
- There is no end/completion screen after the quiz finishes.
|
||||
- Error handling:
|
||||
- Exceptions are caught in ViewModels but not handled (no retry, no error UI, no telemetry hooks
|
||||
yet).
|
||||
|
||||
## Suggested Improvements
|
||||
|
||||
1. Introduce a UI-specific model for the Quiz screen
|
||||
- The domain model `Quiz` is relatively complex and currently used directly in `UiState`.
|
||||
- Add a dedicated, lean UI data class that contains only the data relevant to the quiz screen.
|
||||
- Benefits: Improved clarity for UI developers, simpler previews, easier testing/mocking, and
|
||||
better forward-compatibility when domain evolves.
|
||||
|
||||
2. Expand Unit Test Coverage
|
||||
- Currently there is only one unit test for parsing a sample JSON API response.
|
||||
- Add tests for:
|
||||
- ViewModel state transitions (loading/success/error).
|
||||
- Mapping edge cases (e.g., missing fields, unsupported media types).
|
||||
- Navigation/flow for various question types.
|
||||
|
||||
3. Error Handling Strategy
|
||||
- Map exceptions to user-friendly UI states with retry actions.
|
||||
- Add telemetry/logging hooks for observability.
|
||||
|
||||
4. Feature Completeness
|
||||
- Implement slider answer type.
|
||||
- Add an end/completion screen with score summary and restart/share options.
|
||||
- Consider support for additional media types (video/audio), with graceful fallbacks.
|
||||
5. Transitions between questions could be more smooth.
|
||||
|
||||
## Extra: Related Work I Can Share
|
||||
|
||||
I can share more complex code from my private app that is published on the Google Play Store.
|
||||
Additionally, I have a secondary project — an AI Agent implemented in TypeScript using Google’s
|
||||
GenKit framework — that prepares content for that app. It leverages multiple models, vector stores,
|
||||
and embeddings to orchestrate cooperative behaviors.
|
||||
|
||||
If you’re interested, I can provide a deeper walkthrough, architectural diagrams, or selected code
|
||||
excerpts.
|
||||
|
||||
## License
|
||||
|
||||
This repository is provided as-is for interview and demonstration purposes.
|
@@ -20,17 +20,17 @@ android {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.ui.quiz)
|
||||
implementation(projects.core.designsystem)
|
||||
implementation(projects.domain)
|
||||
|
||||
implementation(projects.model.data)
|
||||
implementation(projects.data)
|
||||
implementation(projects.ui.quiz)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
@@ -8,18 +8,18 @@ naming:
|
||||
|
||||
complexity:
|
||||
LongParameterList:
|
||||
ignoreAnnotated: ['Composable']
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
TooManyFunctions:
|
||||
ignoreAnnotatedFunctions: ['Preview']
|
||||
ignoreAnnotatedFunctions: [ 'Preview' ]
|
||||
|
||||
style:
|
||||
MagicNumber:
|
||||
ignorePropertyDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotated: ['Composable']
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
|
||||
UnusedPrivateMember:
|
||||
ignoreAnnotated: ['Composable']
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
|
||||
# Deviations from defaults
|
||||
formatting:
|
||||
|
158
app/lint-baseline.xml
Normal file
158
app/lint-baseline.xml
Normal file
@@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.13.0-rc02" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-rc02)" variant="all" version="8.13.0-rc02">
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `endX` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:endX="85.84757""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="10"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `endY` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:endY="92.4963""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="11"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `startX` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:startX="42.9492""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="12"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `startY` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:startY="49.59793""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="13"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `offset` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:offset="0.0" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="17"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `offset` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:offset="1.0" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="20"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="nonZero""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_launcher_foreground.xml"
|
||||
line="26"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.purple_200` appears to be unused"
|
||||
errorLine1=" <color name="purple_200">#FFBB86FC</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="3"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.purple_500` appears to be unused"
|
||||
errorLine1=" <color name="purple_500">#FF6200EE</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="4"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.purple_700` appears to be unused"
|
||||
errorLine1=" <color name="purple_700">#FF3700B3</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="5"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.teal_200` appears to be unused"
|
||||
errorLine1=" <color name="teal_200">#FF03DAC5</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="6"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.teal_700` appears to be unused"
|
||||
errorLine1=" <color name="teal_700">#FF018786</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="7"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.black` appears to be unused"
|
||||
errorLine1=" <color name="black">#FF000000</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="8"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.white` appears to be unused"
|
||||
errorLine1=" <color name="white">#FFFFFFFF</color>"
|
||||
errorLine2=" ~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="9"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
@@ -1,13 +1,11 @@
|
||||
package dev.adriankuta.kahootquiz
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
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.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
@@ -1,20 +1,31 @@
|
||||
package dev.adriankuta.kahootquiz
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR
|
||||
|
||||
@Composable
|
||||
fun KahootQuizApp(
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets.safeDrawing,
|
||||
modifier = modifier,
|
||||
) { paddingValues ->
|
||||
KahootQuizNavGraph(modifier = modifier.padding(paddingValues))
|
||||
Image(
|
||||
painter = painterResource(id = DesignR.drawable.bg_image),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
KahootQuizNavGraph(modifier = Modifier.padding(paddingValues))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,8 +16,8 @@ fun KahootQuizNavGraph(
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = QuizRoute,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
) {
|
||||
quizScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.adriankuta.kahootquiz.ui.theme.KahootQuizTheme
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
@@ -4,4 +4,4 @@ import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class MyApplication: Application()
|
||||
class MyApplication : Application()
|
||||
|
@@ -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)
|
@@ -1,58 +0,0 @@
|
||||
package dev.adriankuta.kahootquiz.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun KahootQuizTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
package dev.adriankuta.kahootquiz.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
@@ -1,9 +1,8 @@
|
||||
package dev.adriankuta.kahootquiz
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
@@ -14,4 +13,4 @@ class ExampleUnitTest {
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
core/designsystem/build.gradle.kts
Normal file
7
core/designsystem/build.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kahootquiz.android.library.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.adriankuta.kahootquiz.core.designsystem"
|
||||
}
|
33
core/designsystem/config/detekt/detekt.yml
Normal file
33
core/designsystem/config/detekt/detekt.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
# Exceptions for compose. See https://detekt.dev/docs/introduction/compose
|
||||
naming:
|
||||
FunctionNaming:
|
||||
functionPattern: '[a-zA-Z][a-zA-Z0-9]*'
|
||||
|
||||
TopLevelPropertyNaming:
|
||||
constantPattern: '[A-Z][A-Za-z0-9]*'
|
||||
|
||||
complexity:
|
||||
LongParameterList:
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
TooManyFunctions:
|
||||
ignoreAnnotatedFunctions: [ 'Preview' ]
|
||||
|
||||
style:
|
||||
MagicNumber:
|
||||
ignorePropertyDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
|
||||
UnusedPrivateMember:
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
|
||||
# Deviations from defaults
|
||||
formatting:
|
||||
TrailingCommaOnCallSite:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
useTrailingCommaOnCallSite: true
|
||||
TrailingCommaOnDeclarationSite:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
useTrailingCommaOnDeclarationSite: true
|
176
core/designsystem/lint-baseline.xml
Normal file
176
core/designsystem/lint-baseline.xml
Normal file
@@ -0,0 +1,176 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.13.0-rc02" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-rc02)" variant="all" version="8.13.0-rc02">
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="8"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="12"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="16"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="20"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="24"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="28"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="32"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="36"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="40"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="44"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_type.xml"
|
||||
line="48"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_wrong.xml"
|
||||
line="22"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedAttribute"
|
||||
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
|
||||
errorLine1=" android:fillType="evenOdd""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_wrong.xml"
|
||||
line="27"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="VectorRaster"
|
||||
message="This tag is not supported in images generated from this vector icon for API < 24; check generated icon to make sure it looks acceptable"
|
||||
errorLine1=" <clip-path android:pathData="M5.853,5.721h28.284v28.284h-28.284z" />"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_wrong.xml"
|
||||
line="25"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="VectorRaster"
|
||||
message="This tag is not supported in images generated from this vector icon for API < 24; check generated icon to make sure it looks acceptable"
|
||||
errorLine1=" <clip-path"
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/res/drawable/ic_wrong.xml"
|
||||
line="26"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="IconLocation"
|
||||
message="Found bitmap drawable `res/drawable/bg_image.webp` in densityless folder">
|
||||
<location
|
||||
file="src/main/res/drawable/bg_image.webp"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
@@ -0,0 +1,30 @@
|
||||
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 Blue2 = Color(0xFF1368CE)
|
||||
val Green = Color(0xFF66BF39)
|
||||
val Green2 = Color(0xFF26890C)
|
||||
val Grey = Color(0xFFFAFAFA)
|
||||
val Pink = Color(0xFFFF99AA)
|
||||
val Purple = Color(0xFF864CBF)
|
||||
val Red = Color(0xFFFF3355)
|
||||
val Red2 = Color(0xFFE21B3C)
|
||||
val Yellow3 = Color(0xFFD89E00)
|
@@ -0,0 +1,49 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package dev.adriankuta.kahootquiz.core.designsystem
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun KahootQuizTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = LightColorScheme,
|
||||
typography = Typography,
|
||||
content = content,
|
||||
)
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package dev.adriankuta.kahootquiz.core.designsystem
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
)
|
BIN
core/designsystem/src/main/res/drawable/bg_image.webp
Normal file
BIN
core/designsystem/src/main/res/drawable/bg_image.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
15
core/designsystem/src/main/res/drawable/ic_circle.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_circle.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android: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>
|
23
core/designsystem/src/main/res/drawable/ic_correct.xml
Normal file
23
core/designsystem/src/main/res/drawable/ic_correct.xml
Normal 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>
|
15
core/designsystem/src/main/res/drawable/ic_diamond.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_diamond.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android: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>
|
15
core/designsystem/src/main/res/drawable/ic_square.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_square.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="41dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="41">
|
||||
<path
|
||||
android: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>
|
15
core/designsystem/src/main/res/drawable/ic_triangle.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_triangle.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android: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>
|
50
core/designsystem/src/main/res/drawable/ic_type.xml
Normal file
50
core/designsystem/src/main/res/drawable/ic_type.xml
Normal 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>
|
34
core/designsystem/src/main/res/drawable/ic_wrong.xml
Normal file
34
core/designsystem/src/main/res/drawable/ic_wrong.xml
Normal 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>
|
4
core/network/lint-baseline.xml
Normal file
4
core/network/lint-baseline.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.13.0-rc02" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-rc02)" variant="all" version="8.13.0-rc02">
|
||||
|
||||
</issues>
|
@@ -5,7 +5,7 @@ package dev.adriankuta.kahootquiz.core.network.models
|
||||
data class LanguageInfoDto(
|
||||
val language: String?,
|
||||
val lastUpdatedOn: Long?,
|
||||
val readAloudSupported: Boolean?
|
||||
val readAloudSupported: Boolean?,
|
||||
)
|
||||
|
||||
// Minimal channel info
|
||||
@@ -16,5 +16,5 @@ data class ChannelDto(val id: String?)
|
||||
|
||||
data class PointDto(
|
||||
val x: Int?,
|
||||
val y: Int?
|
||||
val y: Int?,
|
||||
)
|
||||
|
@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.core.network.models
|
||||
|
||||
data class ContentTagsDto(
|
||||
val curriculumCodes: List<String>?,
|
||||
val generatedCurriculumCodes: List<String>?
|
||||
val generatedCurriculumCodes: List<String>?,
|
||||
)
|
||||
|
@@ -13,14 +13,14 @@ data class CoverMetadataDto(
|
||||
val height: Int?,
|
||||
val extractedColors: List<ExtractedColorDto>?,
|
||||
val blurhash: String?,
|
||||
val crop: CropDto?
|
||||
val crop: CropDto?,
|
||||
)
|
||||
|
||||
// Color extracted from cover image
|
||||
|
||||
data class ExtractedColorDto(
|
||||
val swatch: String?,
|
||||
val rgbHex: String?
|
||||
val rgbHex: String?,
|
||||
)
|
||||
|
||||
// Crop descriptor
|
||||
@@ -28,5 +28,5 @@ data class ExtractedColorDto(
|
||||
data class CropDto(
|
||||
val origin: PointDto?,
|
||||
val target: PointDto?,
|
||||
val circular: Boolean?
|
||||
val circular: Boolean?,
|
||||
)
|
||||
|
@@ -7,7 +7,7 @@ data class MetadataDto(
|
||||
val duplicationProtection: Boolean?,
|
||||
val featuredListMemberships: List<FeaturedListMembershipDto>?,
|
||||
val lastEdit: LastEditDto?,
|
||||
val versionMetadata: VersionMetadataDto?
|
||||
val versionMetadata: VersionMetadataDto?,
|
||||
)
|
||||
|
||||
// Access settings
|
||||
@@ -15,14 +15,14 @@ data class MetadataDto(
|
||||
data class AccessDto(
|
||||
val groupRead: List<String>?,
|
||||
val folderGroupIds: List<String>?,
|
||||
val features: List<String>?
|
||||
val features: List<String>?,
|
||||
)
|
||||
|
||||
// Featured list membership
|
||||
|
||||
data class FeaturedListMembershipDto(
|
||||
val list: String?,
|
||||
val addedAt: Long?
|
||||
val addedAt: Long?,
|
||||
)
|
||||
|
||||
// Last edit information
|
||||
@@ -30,7 +30,7 @@ data class FeaturedListMembershipDto(
|
||||
data class LastEditDto(
|
||||
val editorUserId: String?,
|
||||
val editorUsername: String?,
|
||||
val editTimestamp: Long?
|
||||
val editTimestamp: Long?,
|
||||
)
|
||||
|
||||
// Version metadata
|
||||
@@ -38,5 +38,5 @@ data class LastEditDto(
|
||||
data class VersionMetadataDto(
|
||||
val version: Int?,
|
||||
val created: Long?,
|
||||
val creator: String?
|
||||
val creator: String?,
|
||||
)
|
||||
|
@@ -17,15 +17,15 @@ data class QuestionDto(
|
||||
val questionFormat: Int?,
|
||||
val languageInfo: LanguageInfoDto?,
|
||||
val media: List<MediaItemDto>?,
|
||||
val choiceRange: ChoiceRangeDto?
|
||||
val choiceRange: ChoiceRangeDto?,
|
||||
)
|
||||
|
||||
// Choice option
|
||||
|
||||
data class ChoiceDto(
|
||||
val answer: String?,
|
||||
val correct: Boolean?,
|
||||
val languageInfo: LanguageInfoDto?
|
||||
val correct: Boolean,
|
||||
val languageInfo: LanguageInfoDto?,
|
||||
)
|
||||
|
||||
// Optional video attachment
|
||||
@@ -35,7 +35,7 @@ data class VideoDto(
|
||||
val startTime: Int?,
|
||||
val endTime: Int?,
|
||||
val service: String?,
|
||||
val fullUrl: String?
|
||||
val fullUrl: String?,
|
||||
)
|
||||
|
||||
// Image metadata appearing in multiple places
|
||||
@@ -50,7 +50,7 @@ data class ImageMetadataDto(
|
||||
val width: Int? = null,
|
||||
val height: Int? = null,
|
||||
val effects: List<String>? = null,
|
||||
val crop: CropDto? = null
|
||||
val crop: CropDto? = null,
|
||||
)
|
||||
|
||||
// Generic media item on question
|
||||
@@ -67,7 +67,7 @@ data class MediaItemDto(
|
||||
val resources: String? = null,
|
||||
val width: Int? = null,
|
||||
val height: Int? = null,
|
||||
val crop: CropDto? = null
|
||||
val crop: CropDto? = null,
|
||||
)
|
||||
|
||||
// Slider range for "slider" question type
|
||||
@@ -77,5 +77,5 @@ data class ChoiceRangeDto(
|
||||
val end: Int?,
|
||||
val step: Int?,
|
||||
val correct: Int?,
|
||||
val tolerance: Int?
|
||||
val tolerance: Int?,
|
||||
)
|
||||
|
@@ -32,5 +32,5 @@ data class QuizResponseDto(
|
||||
val hasRestrictedContent: Boolean?,
|
||||
val type: String?,
|
||||
val created: Long?,
|
||||
val modified: Long?
|
||||
val modified: Long?,
|
||||
)
|
||||
|
@@ -42,8 +42,14 @@
|
||||
],
|
||||
"blurhash": "UuJ*#Qxtx]xaCAj[W=WqEma}M{R*M|WVn#j?",
|
||||
"crop": {
|
||||
"origin": {"x": 227, "y": 0},
|
||||
"target": {"x": 1948, "y": 1299},
|
||||
"origin": {
|
||||
"x": 227,
|
||||
"y": 0
|
||||
},
|
||||
"target": {
|
||||
"x": 1948,
|
||||
"y": 1299
|
||||
},
|
||||
"circular": false
|
||||
}
|
||||
},
|
||||
@@ -55,8 +61,24 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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",
|
||||
"image": "https://media.kahoot.it/b2709905-1c6e-45a0-9cc1-34c6580495e5",
|
||||
@@ -71,9 +93,18 @@
|
||||
"height": 1406
|
||||
},
|
||||
"resources": "mikroman6/Moment/Getty Images",
|
||||
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -83,15 +114,56 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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 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}}
|
||||
{
|
||||
"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 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": "",
|
||||
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": [
|
||||
{
|
||||
"type": "background_image",
|
||||
@@ -105,7 +177,17 @@
|
||||
"resources": "Nick Brundle Photography/Moment/Getty Images",
|
||||
"width": 2309,
|
||||
"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,
|
||||
"pointsMultiplier": 1,
|
||||
"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": "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}}
|
||||
{
|
||||
"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": "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",
|
||||
"imageMetadata": {
|
||||
@@ -129,9 +243,18 @@
|
||||
"effects": []
|
||||
},
|
||||
"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,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -141,10 +264,42 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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": "Syria", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
|
||||
{"answer": "Iran", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
|
||||
{
|
||||
"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": "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",
|
||||
"imageMetadata": {
|
||||
@@ -154,9 +309,18 @@
|
||||
"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",
|
||||
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -166,10 +330,42 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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": "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}}
|
||||
{
|
||||
"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": "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",
|
||||
"imageMetadata": {
|
||||
@@ -179,9 +375,19 @@
|
||||
"effects": []
|
||||
},
|
||||
"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,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -191,10 +397,42 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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": "Olympia", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
|
||||
{"answer": "Delphi", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
|
||||
{
|
||||
"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": "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",
|
||||
"imageMetadata": {
|
||||
@@ -202,12 +440,31 @@
|
||||
"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",
|
||||
"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",
|
||||
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -217,10 +474,42 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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": "Cyrus", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
|
||||
{"answer": "Mausoleus", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
|
||||
{
|
||||
"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": "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",
|
||||
"imageMetadata": {
|
||||
@@ -234,9 +523,18 @@
|
||||
"height": 1414
|
||||
},
|
||||
"resources": "MirageC/Moment/Getty Images",
|
||||
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -245,8 +543,24 @@
|
||||
"time": 60000,
|
||||
"pointsMultiplier": 2,
|
||||
"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",
|
||||
"imageMetadata": {
|
||||
@@ -254,12 +568,32 @@
|
||||
"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",
|
||||
"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",
|
||||
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"id": "",
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -269,10 +603,42 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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": "Tidal Wave", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
|
||||
{"answer": "Storm", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
|
||||
{
|
||||
"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": "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",
|
||||
"imageMetadata": {
|
||||
@@ -280,12 +646,31 @@
|
||||
"contentType": "image/*",
|
||||
"resources": "Emad Victor SHENOUDA [Attribution], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/3/33/PHAROS2013-3000x2250.jpg",
|
||||
"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",
|
||||
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -295,10 +680,42 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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 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}}
|
||||
{
|
||||
"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 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",
|
||||
"imageMetadata": {
|
||||
@@ -306,12 +723,32 @@
|
||||
"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",
|
||||
"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",
|
||||
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"id": "",
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -319,7 +756,13 @@
|
||||
"question": "How many of the Seven Wonders still exist?",
|
||||
"time": 20000,
|
||||
"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",
|
||||
"imageMetadata": {
|
||||
"id": "b431b3aa-4a46-49c9-b4ac-aa1dde40333f",
|
||||
@@ -328,9 +771,19 @@
|
||||
"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",
|
||||
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"id": "",
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
},
|
||||
{
|
||||
@@ -340,10 +793,42 @@
|
||||
"points": true,
|
||||
"pointsMultiplier": 1,
|
||||
"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 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}}
|
||||
{
|
||||
"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 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",
|
||||
"imageMetadata": {
|
||||
@@ -351,16 +836,39 @@
|
||||
"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",
|
||||
"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",
|
||||
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
|
||||
"video": {
|
||||
"id": "",
|
||||
"startTime": 0,
|
||||
"endTime": 0,
|
||||
"service": "youtube",
|
||||
"fullUrl": ""
|
||||
},
|
||||
"questionFormat": 0,
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"media": []
|
||||
}
|
||||
],
|
||||
"contentTags": {"curriculumCodes": [], "generatedCurriculumCodes": []},
|
||||
"contentTags": {
|
||||
"curriculumCodes": [],
|
||||
"generatedCurriculumCodes": []
|
||||
},
|
||||
"metadata": {
|
||||
"access": {
|
||||
"groupRead": [
|
||||
@@ -372,12 +880,20 @@
|
||||
"36022fd9-43e1-4b36-9c98-a6a3b2b53038"
|
||||
],
|
||||
"folderGroupIds": [],
|
||||
"features": ["PremiumEduContent"]
|
||||
"features": [
|
||||
"PremiumEduContent"
|
||||
]
|
||||
},
|
||||
"duplicationProtection": true,
|
||||
"featuredListMemberships": [
|
||||
{"list": "youngfeatured", "addedAt": 1682336780289},
|
||||
{"list": "featured", "addedAt": 1682336738189}
|
||||
{
|
||||
"list": "youngfeatured",
|
||||
"addedAt": 1682336780289
|
||||
},
|
||||
{
|
||||
"list": "featured",
|
||||
"addedAt": 1682336738189
|
||||
}
|
||||
],
|
||||
"lastEdit": {
|
||||
"editorUserId": "4c1574ee-de54-40a2-be15-8d72b333afad",
|
||||
@@ -392,9 +908,17 @@
|
||||
},
|
||||
"resources": "Nick Brundle Photography/Moment/Getty Images",
|
||||
"slug": "seven-wonders-of-the-ancient-world",
|
||||
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
|
||||
"languageInfo": {
|
||||
"language": "en-US",
|
||||
"lastUpdatedOn": 1741920189202,
|
||||
"readAloudSupported": true
|
||||
},
|
||||
"inventoryItemIds": [],
|
||||
"channels": [{"id": "247c3eb4-af80-4c1f-b006-558682c7bd2f"}],
|
||||
"channels": [
|
||||
{
|
||||
"id": "247c3eb4-af80-4c1f-b006-558682c7bd2f"
|
||||
}
|
||||
],
|
||||
"isValid": true,
|
||||
"playAsGuest": true,
|
||||
"hasRestrictedContent": false,
|
||||
|
@@ -4,7 +4,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.adriankuta.kahootquiz.model.data"
|
||||
namespace = "dev.adriankuta.kahootquiz.data"
|
||||
}
|
||||
|
||||
dependencies {
|
5
data/lint-baseline.xml
Normal file
5
data/lint-baseline.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues name="AGP (8.13.0-rc02)" by="lint 8.13.0-rc02" client="gradle" dependencies="false" format="6"
|
||||
type="baseline" variant="all" version="8.13.0-rc02">
|
||||
|
||||
</issues>
|
@@ -1,13 +1,13 @@
|
||||
package dev.adriankuta.kahootquiz.model.data
|
||||
package dev.adriankuta.kahootquiz.data
|
||||
|
||||
import dev.adriankuta.kahootquiz.core.network.retrofit.QuizApi
|
||||
import dev.adriankuta.kahootquiz.data.mappers.toDomainModel
|
||||
import dev.adriankuta.kahootquiz.domain.models.Quiz
|
||||
import dev.adriankuta.kahootquiz.domain.repositories.QuizRepository
|
||||
import dev.adriankuta.kahootquiz.model.data.mappers.toDomainModel
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class QuizRepositoryImpl @Inject constructor(
|
||||
private val quizApi: QuizApi
|
||||
private val quizApi: QuizApi,
|
||||
) : QuizRepository {
|
||||
|
||||
override suspend fun getQuiz(): Quiz {
|
@@ -1,11 +1,11 @@
|
||||
package dev.adriankuta.kahootquiz.model.data.di
|
||||
package dev.adriankuta.kahootquiz.data.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.adriankuta.kahootquiz.data.QuizRepositoryImpl
|
||||
import dev.adriankuta.kahootquiz.domain.repositories.QuizRepository
|
||||
import dev.adriankuta.kahootquiz.model.data.QuizRepositoryImpl
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -15,6 +15,6 @@ internal abstract class RepositoryModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindsQuizRepository(
|
||||
quizRepositoryImpl: QuizRepositoryImpl
|
||||
quizRepositoryImpl: QuizRepositoryImpl,
|
||||
): QuizRepository
|
||||
}
|
@@ -1,7 +1,46 @@
|
||||
package dev.adriankuta.kahootquiz.model.data.mappers
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
import dev.adriankuta.kahootquiz.core.network.models.*
|
||||
import dev.adriankuta.kahootquiz.domain.models.*
|
||||
package dev.adriankuta.kahootquiz.data.mappers
|
||||
|
||||
import dev.adriankuta.kahootquiz.core.network.models.AccessDto
|
||||
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
|
||||
|
||||
internal fun QuizResponseDto.toDomainModel(): Quiz =
|
||||
@@ -34,7 +73,7 @@ internal fun QuizResponseDto.toDomainModel(): Quiz =
|
||||
hasRestrictedContent = hasRestrictedContent,
|
||||
type = type,
|
||||
created = created,
|
||||
modified = modified
|
||||
modified = modified,
|
||||
)
|
||||
|
||||
private fun CoverMetadataDto.toDomain(): CoverMetadata = CoverMetadata(
|
||||
@@ -48,25 +87,25 @@ private fun CoverMetadataDto.toDomain(): CoverMetadata = CoverMetadata(
|
||||
height = height,
|
||||
extractedColors = extractedColors?.map { it.toDomain() },
|
||||
blurhash = blurhash,
|
||||
crop = crop?.toDomain()
|
||||
crop = crop?.toDomain(),
|
||||
)
|
||||
|
||||
private fun ExtractedColorDto.toDomain(): ExtractedColor = ExtractedColor(
|
||||
swatch = swatch,
|
||||
rgbHex = rgbHex
|
||||
rgbHex = rgbHex,
|
||||
)
|
||||
|
||||
private fun CropDto.toDomain(): Crop = Crop(
|
||||
origin = origin?.toDomain(),
|
||||
target = target?.toDomain(),
|
||||
circular = circular
|
||||
circular = circular,
|
||||
)
|
||||
|
||||
private fun PointDto.toDomain(): Point = Point(x = x, y = y)
|
||||
|
||||
private fun ContentTagsDto.toDomain(): ContentTags = ContentTags(
|
||||
curriculumCodes = curriculumCodes,
|
||||
generatedCurriculumCodes = generatedCurriculumCodes
|
||||
generatedCurriculumCodes = generatedCurriculumCodes,
|
||||
)
|
||||
|
||||
private fun MetadataDto.toDomain(): Metadata = Metadata(
|
||||
@@ -74,36 +113,36 @@ private fun MetadataDto.toDomain(): Metadata = Metadata(
|
||||
duplicationProtection = duplicationProtection,
|
||||
featuredListMemberships = featuredListMemberships?.map { it.toDomain() },
|
||||
lastEdit = lastEdit?.toDomain(),
|
||||
versionMetadata = versionMetadata?.toDomain()
|
||||
versionMetadata = versionMetadata?.toDomain(),
|
||||
)
|
||||
|
||||
private fun AccessDto.toDomain(): Access = Access(
|
||||
groupRead = groupRead,
|
||||
folderGroupIds = folderGroupIds,
|
||||
features = features
|
||||
features = features,
|
||||
)
|
||||
|
||||
private fun FeaturedListMembershipDto.toDomain(): FeaturedListMembership = FeaturedListMembership(
|
||||
list = list,
|
||||
addedAt = addedAt
|
||||
addedAt = addedAt,
|
||||
)
|
||||
|
||||
private fun LastEditDto.toDomain(): LastEdit = LastEdit(
|
||||
editorUserId = editorUserId,
|
||||
editorUsername = editorUsername,
|
||||
editTimestamp = editTimestamp
|
||||
editTimestamp = editTimestamp,
|
||||
)
|
||||
|
||||
private fun VersionMetadataDto.toDomain(): VersionMetadata = VersionMetadata(
|
||||
version = version,
|
||||
created = created,
|
||||
creator = creator
|
||||
creator = creator,
|
||||
)
|
||||
|
||||
private fun LanguageInfoDto.toDomain(): LanguageInfo = LanguageInfo(
|
||||
language = language,
|
||||
lastUpdatedOn = lastUpdatedOn,
|
||||
readAloudSupported = readAloudSupported
|
||||
readAloudSupported = readAloudSupported,
|
||||
)
|
||||
|
||||
private fun ChannelDto.toDomain(): Channel = Channel(id = id)
|
||||
@@ -114,7 +153,7 @@ private fun QuestionDto.toDomain(): Question = Question(
|
||||
time = time?.milliseconds,
|
||||
points = points,
|
||||
pointsMultiplier = pointsMultiplier,
|
||||
choices = choices?.map { it.toDomain() },
|
||||
choices = choices?.map { it.toDomain() }.orEmpty(),
|
||||
layout = layout,
|
||||
image = image,
|
||||
imageMetadata = imageMetadata?.toDomain(),
|
||||
@@ -123,13 +162,13 @@ private fun QuestionDto.toDomain(): Question = Question(
|
||||
questionFormat = questionFormat,
|
||||
languageInfo = languageInfo?.toDomain(),
|
||||
media = media?.map { it.toDomain() },
|
||||
choiceRange = choiceRange?.toDomain()
|
||||
choiceRange = choiceRange?.toDomain(),
|
||||
)
|
||||
|
||||
private fun ChoiceDto.toDomain(): Choice = Choice(
|
||||
answer = answer,
|
||||
correct = correct,
|
||||
languageInfo = languageInfo?.toDomain()
|
||||
languageInfo = languageInfo?.toDomain(),
|
||||
)
|
||||
|
||||
private fun VideoDto.toDomain(): Video = Video(
|
||||
@@ -137,7 +176,7 @@ private fun VideoDto.toDomain(): Video = Video(
|
||||
startTime = startTime,
|
||||
endTime = endTime,
|
||||
service = service,
|
||||
fullUrl = fullUrl
|
||||
fullUrl = fullUrl,
|
||||
)
|
||||
|
||||
private fun ImageMetadataDto.toDomain(): ImageMetadata = ImageMetadata(
|
||||
@@ -150,7 +189,7 @@ private fun ImageMetadataDto.toDomain(): ImageMetadata = ImageMetadata(
|
||||
width = width,
|
||||
height = height,
|
||||
effects = effects,
|
||||
crop = crop?.toDomain()
|
||||
crop = crop?.toDomain(),
|
||||
)
|
||||
|
||||
private fun MediaItemDto.toDomain(): MediaItem = MediaItem(
|
||||
@@ -165,7 +204,7 @@ private fun MediaItemDto.toDomain(): MediaItem = MediaItem(
|
||||
resources = resources,
|
||||
width = width,
|
||||
height = height,
|
||||
crop = crop?.toDomain()
|
||||
crop = crop?.toDomain(),
|
||||
)
|
||||
|
||||
private fun ChoiceRangeDto.toDomain(): ChoiceRange = ChoiceRange(
|
||||
@@ -173,5 +212,5 @@ private fun ChoiceRangeDto.toDomain(): ChoiceRange = ChoiceRange(
|
||||
end = end,
|
||||
step = step,
|
||||
correct = correct,
|
||||
tolerance = tolerance
|
||||
tolerance = tolerance,
|
||||
)
|
4
domain/lint-baseline.xml
Normal file
4
domain/lint-baseline.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.13.0-rc02" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-rc02)" variant="all" version="8.13.0-rc02">
|
||||
|
||||
</issues>
|
@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
data class Access(
|
||||
val groupRead: List<String>?,
|
||||
val folderGroupIds: List<String>?,
|
||||
val features: List<String>?
|
||||
)
|
||||
val features: List<String>?,
|
||||
)
|
||||
|
@@ -2,4 +2,4 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Minimal channel info
|
||||
|
||||
data class Channel(val id: String?)
|
||||
data class Channel(val id: String?)
|
||||
|
@@ -2,6 +2,6 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
data class Choice(
|
||||
val answer: String?,
|
||||
val correct: Boolean?,
|
||||
val languageInfo: LanguageInfo?
|
||||
)
|
||||
val correct: Boolean,
|
||||
val languageInfo: LanguageInfo? = null,
|
||||
)
|
||||
|
@@ -7,5 +7,5 @@ data class ChoiceRange(
|
||||
val end: Int?,
|
||||
val step: Int?,
|
||||
val correct: Int?,
|
||||
val tolerance: Int?
|
||||
)
|
||||
val tolerance: Int?,
|
||||
)
|
||||
|
@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
data class ContentTags(
|
||||
val curriculumCodes: List<String>?,
|
||||
val generatedCurriculumCodes: List<String>?
|
||||
)
|
||||
val generatedCurriculumCodes: List<String>?,
|
||||
)
|
||||
|
@@ -13,5 +13,5 @@ data class CoverMetadata(
|
||||
val height: Int?,
|
||||
val extractedColors: List<ExtractedColor>?,
|
||||
val blurhash: String?,
|
||||
val crop: Crop?
|
||||
)
|
||||
val crop: Crop?,
|
||||
)
|
||||
|
@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
data class Crop(
|
||||
val origin: Point?,
|
||||
val target: Point?,
|
||||
val circular: Boolean?
|
||||
)
|
||||
val circular: Boolean?,
|
||||
)
|
||||
|
@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
data class ExtractedColor(
|
||||
val swatch: String?,
|
||||
val rgbHex: String?
|
||||
)
|
||||
val rgbHex: String?,
|
||||
)
|
||||
|
@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
data class FeaturedListMembership(
|
||||
val list: String?,
|
||||
val addedAt: Long?
|
||||
)
|
||||
val addedAt: Long?,
|
||||
)
|
||||
|
@@ -12,5 +12,5 @@ data class ImageMetadata(
|
||||
val width: Int? = null,
|
||||
val height: Int? = null,
|
||||
val effects: List<String>? = null,
|
||||
val crop: Crop? = null
|
||||
)
|
||||
val crop: Crop? = null,
|
||||
)
|
||||
|
@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
data class LanguageInfo(
|
||||
val language: String?,
|
||||
val lastUpdatedOn: Long?,
|
||||
val readAloudSupported: Boolean?
|
||||
)
|
||||
val readAloudSupported: Boolean?,
|
||||
)
|
||||
|
@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
data class LastEdit(
|
||||
val editorUserId: String?,
|
||||
val editorUsername: String?,
|
||||
val editTimestamp: Long?
|
||||
)
|
||||
val editTimestamp: Long?,
|
||||
)
|
||||
|
@@ -14,5 +14,5 @@ data class MediaItem(
|
||||
val resources: String? = null,
|
||||
val width: Int? = null,
|
||||
val height: Int? = null,
|
||||
val crop: Crop? = null
|
||||
)
|
||||
val crop: Crop? = null,
|
||||
)
|
||||
|
@@ -7,5 +7,5 @@ data class Metadata(
|
||||
val duplicationProtection: Boolean?,
|
||||
val featuredListMemberships: List<FeaturedListMembership>?,
|
||||
val lastEdit: LastEdit?,
|
||||
val versionMetadata: VersionMetadata?
|
||||
)
|
||||
val versionMetadata: VersionMetadata?,
|
||||
)
|
||||
|
@@ -4,5 +4,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
data class Point(
|
||||
val x: Int?,
|
||||
val y: Int?
|
||||
)
|
||||
val y: Int?,
|
||||
)
|
||||
|
@@ -8,16 +8,16 @@ data class Question(
|
||||
val type: String?,
|
||||
val question: String?,
|
||||
val time: Duration?,
|
||||
val points: Boolean?,
|
||||
val points: Boolean? = null,
|
||||
val pointsMultiplier: Int?,
|
||||
val choices: List<Choice>?,
|
||||
val layout: String?,
|
||||
val image: String?,
|
||||
val choices: List<Choice>,
|
||||
val layout: String? = null,
|
||||
val image: String? = null,
|
||||
val imageMetadata: ImageMetadata?,
|
||||
val resources: String?,
|
||||
val video: Video?,
|
||||
val questionFormat: Int?,
|
||||
val languageInfo: LanguageInfo?,
|
||||
val media: List<MediaItem>?,
|
||||
val choiceRange: ChoiceRange?
|
||||
)
|
||||
val resources: String? = null,
|
||||
val video: Video? = null,
|
||||
val questionFormat: Int? = null,
|
||||
val languageInfo: LanguageInfo? = null,
|
||||
val media: List<MediaItem>? = null,
|
||||
val choiceRange: ChoiceRange? = null,
|
||||
)
|
||||
|
@@ -31,5 +31,5 @@ data class Quiz(
|
||||
val hasRestrictedContent: Boolean?,
|
||||
val type: String?,
|
||||
val created: Long?,
|
||||
val modified: Long?
|
||||
)
|
||||
val modified: Long?,
|
||||
)
|
||||
|
@@ -5,5 +5,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
data class VersionMetadata(
|
||||
val version: Int?,
|
||||
val created: Long?,
|
||||
val creator: String?
|
||||
)
|
||||
val creator: String?,
|
||||
)
|
||||
|
@@ -5,5 +5,5 @@ data class Video(
|
||||
val startTime: Int?,
|
||||
val endTime: Int?,
|
||||
val service: String?,
|
||||
val fullUrl: String?
|
||||
)
|
||||
val fullUrl: String?,
|
||||
)
|
||||
|
@@ -5,7 +5,7 @@ import dev.adriankuta.kahootquiz.domain.repositories.QuizRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetQuizUseCase @Inject constructor(
|
||||
private val quizRepository: QuizRepository
|
||||
private val quizRepository: QuizRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Quiz {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
[versions]
|
||||
retrofit = "3.0.0"
|
||||
targetSdk = "36"
|
||||
compileSdk = "36"
|
||||
minSdk = "23"
|
||||
@@ -21,6 +20,8 @@ animation = "1.9.0"
|
||||
appUpdateKtx = "2.1.0"
|
||||
appcompat = "1.7.1"
|
||||
billing = "8.0.0"
|
||||
coilCompose = "3.3.0"
|
||||
coilNetworkOkhttp = "3.3.0"
|
||||
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
|
||||
datetime = "0.7.1" # https://github.com/Kotlin/kotlinx-datetime/releases
|
||||
@@ -46,6 +47,7 @@ material = "1.12.0"
|
||||
materialIconsExtended = "1.7.8"
|
||||
mockk = "1.14.5" # https://github.com/mockk/mockk/releases
|
||||
playServicesAds = "24.5.0"
|
||||
retrofit = "3.0.0"
|
||||
reviewKtx = "2.0.2"
|
||||
room = "2.7.2"
|
||||
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" }
|
||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
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-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
|
||||
dotlottie-android = { module = "com.github.LottieFiles:dotlottie-android", version.ref = "dotlottieAndroid" }
|
||||
|
@@ -25,7 +25,8 @@ rootProject.name = "KahootQuiz"
|
||||
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
include(":app")
|
||||
include(":core:designsystem")
|
||||
include(":core:network")
|
||||
include(":data")
|
||||
include(":domain")
|
||||
include(":model:data")
|
||||
include(":ui:quiz")
|
||||
|
@@ -9,7 +9,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.designsystem)
|
||||
implementation(projects.domain)
|
||||
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
}
|
||||
|
@@ -1,3 +1,26 @@
|
||||
# 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:
|
||||
|
4
ui/quiz/lint-baseline.xml
Normal file
4
ui/quiz/lint-baseline.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.13.0-rc02" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-rc02)" variant="all" version="8.13.0-rc02">
|
||||
|
||||
</issues>
|
@@ -1,33 +1,216 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Grey
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
|
||||
import dev.adriankuta.kahootquiz.domain.models.Choice
|
||||
import dev.adriankuta.kahootquiz.domain.models.Question
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.components.AnswerFeedbackBanner
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.components.Choices
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.components.QuestionContent
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.components.TimerBar
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.components.Toolbar
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun QuizScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: QuizScreenViewModel = hiltViewModel()
|
||||
viewModel: QuizScreenViewModel = hiltViewModel(),
|
||||
) {
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
QuizScreen(
|
||||
uiState = uiState,
|
||||
modifier = modifier
|
||||
onSelect = viewModel::onChoiceSelected,
|
||||
onContinue = viewModel::onContinue,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuizScreen(
|
||||
uiState: QuizUiState,
|
||||
uiState: ScreenUiState,
|
||||
onSelect: (Int) -> Unit,
|
||||
onContinue: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier) {
|
||||
Text(uiState.quiz?.id?.value ?: "")
|
||||
Box(modifier.fillMaxSize()) {
|
||||
when (uiState) {
|
||||
ScreenUiState.Loading -> QuizScreenLoading()
|
||||
is ScreenUiState.Success -> QuizScreenSuccess(
|
||||
uiState = uiState,
|
||||
onSelect = onSelect,
|
||||
onContinue = onContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuizScreenLoading(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun QuizScreenSuccess(
|
||||
uiState: ScreenUiState.Success,
|
||||
onSelect: (Int) -> Unit,
|
||||
onContinue: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(72.dp),
|
||||
) {
|
||||
Toolbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp),
|
||||
currentQuestionIndex = uiState.currentQuestionIndex,
|
||||
totalQuestions = uiState.totalQuestions,
|
||||
)
|
||||
uiState.isAnswerCorrect?.let { isCorrect ->
|
||||
AnswerFeedbackBanner(
|
||||
isCorrect = isCorrect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
QuestionContent(
|
||||
question = uiState.currentQuestion,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxHeight(0.5f),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Choices(
|
||||
choices = uiState.currentQuestion.choices,
|
||||
selectedChoiceIndex = uiState.selectedChoiceIndex,
|
||||
onSelect = onSelect,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
|
||||
// Timer below choices
|
||||
if (uiState.selectedChoiceIndex == null && uiState.timerState.totalTimeSeconds > 0) {
|
||||
TimerBar(
|
||||
totalSeconds = uiState.timerState.totalTimeSeconds,
|
||||
remainingSeconds = uiState.timerState.remainingTimeSeconds,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = onContinue,
|
||||
colors = ButtonDefaults.filledTonalButtonColors().copy(
|
||||
containerColor = Grey,
|
||||
contentColor = Color.Black,
|
||||
),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.continue_text),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 = ScreenUiState.Success(
|
||||
currentQuestion = sampleQuestion,
|
||||
selectedChoiceIndex = null,
|
||||
totalQuestions = 12,
|
||||
),
|
||||
onSelect = {},
|
||||
onContinue = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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 = ScreenUiState.Success(
|
||||
currentQuestion = sampleQuestion,
|
||||
selectedChoiceIndex = 1,
|
||||
totalQuestions = 12,
|
||||
),
|
||||
onSelect = {},
|
||||
onContinue = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -3,29 +3,184 @@ package dev.adriankuta.kahootquiz.ui.quiz
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.adriankuta.kahootquiz.domain.models.Question
|
||||
import dev.adriankuta.kahootquiz.domain.models.Quiz
|
||||
import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.utils.Result
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.utils.asResult
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class QuizScreenViewModel @Inject constructor(
|
||||
private val getQuizUseCase: GetQuizUseCase
|
||||
private val getQuizUseCase: GetQuizUseCase,
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(QuizUiState())
|
||||
val uiState: StateFlow<QuizUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val quiz: StateFlow<QuizUiState> = flow {
|
||||
emit(QuizUiState.Success(getQuizUseCase()))
|
||||
}
|
||||
.asResult()
|
||||
.map { quizResult ->
|
||||
when (quizResult) {
|
||||
is Result.Error -> QuizUiState.Loading // Todo error handling not implemented on UI
|
||||
Result.Loading -> QuizUiState.Loading
|
||||
is Result.Success -> quizResult.data
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = QuizUiState.Loading,
|
||||
)
|
||||
private val _selectedChoiceIndex = MutableStateFlow<Int?>(null)
|
||||
private val _remainingTimeSeconds = MutableStateFlow(0)
|
||||
private val _currentQuestionIndex = MutableStateFlow(0)
|
||||
private var timerJob: Job? = null
|
||||
|
||||
init {
|
||||
// Start timer when the first question is displayed (on initial quiz load)
|
||||
viewModelScope.launch {
|
||||
_uiState.value = QuizUiState(getQuizUseCase())
|
||||
quiz.collect { quizState ->
|
||||
if (quizState is QuizUiState.Success) {
|
||||
// Start only if timer hasn't been started yet and we are on the first question
|
||||
if (timerJob == null && _currentQuestionIndex.value == 0) {
|
||||
val firstQuestionTime =
|
||||
quizState.quiz.questions.getOrNull(0)?.time?.inWholeSeconds?.toInt()
|
||||
startCountdown(firstQuestionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val uiState: StateFlow<ScreenUiState> = screenUiState(
|
||||
quizFlow = quiz,
|
||||
selectedChoiceIndexFlow = _selectedChoiceIndex,
|
||||
remainingTimeSecondsFlow = _remainingTimeSeconds,
|
||||
currentQuestionIndexFlow = _currentQuestionIndex,
|
||||
)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = ScreenUiState.Loading,
|
||||
)
|
||||
|
||||
fun onChoiceSelected(index: Int) {
|
||||
timerJob?.cancel()
|
||||
timerJob = null
|
||||
_selectedChoiceIndex.value = index
|
||||
}
|
||||
|
||||
fun onContinue() {
|
||||
val quizState = quiz.value
|
||||
if (quizState is QuizUiState.Success) {
|
||||
val total = quizState.quiz.questions.size
|
||||
val current = _currentQuestionIndex.value
|
||||
val nextIndex = current + 1
|
||||
if (nextIndex < total) {
|
||||
_selectedChoiceIndex.value = null
|
||||
_currentQuestionIndex.value = nextIndex
|
||||
val nextQuestionTime =
|
||||
quizState.quiz.questions[nextIndex].time?.inWholeSeconds?.toInt()
|
||||
startCountdown(nextQuestionTime)
|
||||
} else {
|
||||
// Last question reached: stop timer and keep state (could navigate to results in the future)
|
||||
timerJob?.cancel()
|
||||
timerJob = null
|
||||
_remainingTimeSeconds.value = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCountdown(totalSeconds: Int?) {
|
||||
timerJob?.cancel()
|
||||
if (totalSeconds == null || totalSeconds <= 0) {
|
||||
_remainingTimeSeconds.value = 0
|
||||
timerJob = null
|
||||
return
|
||||
}
|
||||
_remainingTimeSeconds.value = totalSeconds
|
||||
timerJob = viewModelScope.launch {
|
||||
var remaining = totalSeconds
|
||||
while (remaining > 0) {
|
||||
delay(1.seconds)
|
||||
remaining -= 1
|
||||
_remainingTimeSeconds.value = remaining
|
||||
}
|
||||
// Time out: reveal answers without a selection
|
||||
if (_selectedChoiceIndex.value == null) {
|
||||
_selectedChoiceIndex.value = -1
|
||||
}
|
||||
timerJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class QuizUiState(
|
||||
val quiz: Quiz? = null
|
||||
)
|
||||
private fun screenUiState(
|
||||
quizFlow: StateFlow<QuizUiState>,
|
||||
selectedChoiceIndexFlow: Flow<Int?>,
|
||||
remainingTimeSecondsFlow: Flow<Int>,
|
||||
currentQuestionIndexFlow: Flow<Int>,
|
||||
): Flow<ScreenUiState> = combine(
|
||||
quizFlow,
|
||||
selectedChoiceIndexFlow,
|
||||
remainingTimeSecondsFlow,
|
||||
currentQuestionIndexFlow,
|
||||
) { quizState, selectedChoiceIndex, remainingTimeSeconds, currentQuestionIndex ->
|
||||
when (quizState) {
|
||||
QuizUiState.Loading -> ScreenUiState.Loading
|
||||
is QuizUiState.Success -> {
|
||||
val currentQuestion = quizState.quiz.questions[currentQuestionIndex]
|
||||
val isAnswerCorrect = selectedChoiceIndex?.let { idx ->
|
||||
currentQuestion.choices?.getOrNull(idx)?.correct == true
|
||||
}
|
||||
|
||||
ScreenUiState.Success(
|
||||
currentQuestion = currentQuestion,
|
||||
selectedChoiceIndex = selectedChoiceIndex,
|
||||
currentQuestionIndex = currentQuestionIndex,
|
||||
totalQuestions = quizState.quiz.questions.size,
|
||||
timerState = TimerState(
|
||||
remainingTimeSeconds = remainingTimeSeconds,
|
||||
totalTimeSeconds = currentQuestion.time?.inWholeSeconds?.toInt() ?: 0,
|
||||
),
|
||||
isAnswerCorrect = isAnswerCorrect,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface QuizUiState {
|
||||
data object Loading : QuizUiState
|
||||
data class Success(
|
||||
val quiz: Quiz,
|
||||
) : QuizUiState
|
||||
}
|
||||
|
||||
sealed interface ScreenUiState {
|
||||
data object Loading : ScreenUiState
|
||||
data class Success(
|
||||
val currentQuestion: Question,
|
||||
val selectedChoiceIndex: Int? = null,
|
||||
val currentQuestionIndex: Int = 0,
|
||||
val totalQuestions: Int = 0,
|
||||
val timerState: TimerState = TimerState(),
|
||||
val isAnswerCorrect: Boolean? = null,
|
||||
) : ScreenUiState
|
||||
}
|
||||
|
||||
data class TimerState(
|
||||
val remainingTimeSeconds: Int = 0,
|
||||
val totalTimeSeconds: Int = 0,
|
||||
)
|
||||
|
@@ -0,0 +1,176 @@
|
||||
@file:OptIn(ExperimentalLayoutApi::class)
|
||||
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Blue2
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Green
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Green2
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Pink
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Red
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Red2
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Yellow3
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.contrastiveTo
|
||||
import dev.adriankuta.kahootquiz.domain.models.Choice
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR
|
||||
|
||||
@Composable
|
||||
fun Choices(
|
||||
choices: List<Choice>,
|
||||
onSelect: (Int) -> Unit,
|
||||
selectedChoiceIndex: Int?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
EvenGrid(
|
||||
items = choices,
|
||||
columns = 2,
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) { choice, index ->
|
||||
ChoiceItem(
|
||||
choice = choice,
|
||||
index = index,
|
||||
selectedChoiceIndex = selectedChoiceIndex,
|
||||
onClick = { onSelect(index) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChoiceItem(
|
||||
choice: Choice,
|
||||
onClick: () -> Unit,
|
||||
index: Int,
|
||||
selectedChoiceIndex: Int?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (selectedChoiceIndex != null) {
|
||||
ChoiceItemRevealed(
|
||||
choice = choice,
|
||||
index = index,
|
||||
isSelected = selectedChoiceIndex == index,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
ChoiceItemDefault(
|
||||
choice = choice,
|
||||
index = index,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChoiceItemDefault(
|
||||
choice: Choice,
|
||||
index: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backgroundColor = when (index) {
|
||||
0 -> Red2
|
||||
1 -> Blue2
|
||||
2 -> Yellow3
|
||||
3 -> Green2
|
||||
else -> Color.Gray
|
||||
}
|
||||
|
||||
val icon = when (index) {
|
||||
0 -> DesignR.drawable.ic_triangle
|
||||
1 -> DesignR.drawable.ic_diamond
|
||||
2 -> DesignR.drawable.ic_circle
|
||||
else -> DesignR.drawable.ic_square
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(backgroundColor, shape = RoundedCornerShape(4.dp))
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.size(32.dp),
|
||||
)
|
||||
Text(
|
||||
text = choice.answer ?: "",
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
color = contrastiveTo(backgroundColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChoiceItemRevealed(
|
||||
choice: Choice,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backgroundColor = when {
|
||||
isSelected && !choice.correct -> Red
|
||||
choice.correct -> Green
|
||||
else -> Pink
|
||||
}
|
||||
|
||||
val icon = if (choice.correct) {
|
||||
DesignR.drawable.ic_correct
|
||||
} else {
|
||||
DesignR.drawable.ic_wrong
|
||||
}
|
||||
|
||||
val alignment = if (index % 2 == 0) {
|
||||
Alignment.TopStart
|
||||
} else {
|
||||
Alignment.TopEnd
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(backgroundColor, shape = RoundedCornerShape(4.dp)),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(alignment)
|
||||
.offset(
|
||||
x = if (alignment == Alignment.TopStart) (-8).dp else (8).dp,
|
||||
y = (-8).dp,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = choice.answer ?: "",
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
color = contrastiveTo(backgroundColor),
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Green
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Red
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.R
|
||||
|
||||
@Composable
|
||||
fun AnswerFeedbackBanner(
|
||||
isCorrect: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(10f),
|
||||
shadowElevation = 8.dp,
|
||||
color = if (isCorrect) Green else Red,
|
||||
contentColor = Color.White,
|
||||
) {
|
||||
Box {
|
||||
Text(
|
||||
text = stringResource(if (isCorrect) R.string.correct else R.string.wrong),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun <T> EvenGrid(
|
||||
items: List<T>,
|
||||
columns: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
content: @Composable RowScope.(item: T, index: Int) -> Unit,
|
||||
) {
|
||||
val rows = (items.size + columns - 1) / columns // total rows needed
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = verticalArrangement,
|
||||
) {
|
||||
repeat(rows) { rowIndex ->
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = horizontalArrangement,
|
||||
) {
|
||||
repeat(columns) { columnIndex ->
|
||||
val itemIndex = rowIndex * columns + columnIndex
|
||||
if (itemIndex < items.size) {
|
||||
content(items[itemIndex], itemIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.HtmlCompat
|
||||
import coil3.compose.AsyncImage
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.toAnnotatedString
|
||||
import dev.adriankuta.kahootquiz.domain.models.Question
|
||||
|
||||
@Composable
|
||||
fun QuestionContent(
|
||||
question: Question,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = question.image,
|
||||
contentDescription = question.imageMetadata?.altText,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.clip(shape = RoundedCornerShape(4.dp)),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val questionText = androidx.compose.runtime.remember(question.question) {
|
||||
HtmlCompat.fromHtml(
|
||||
question.question ?: "",
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT,
|
||||
).toAnnotatedString()
|
||||
}
|
||||
Text(
|
||||
text = questionText,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Purple
|
||||
|
||||
@Composable
|
||||
fun TimerBar(
|
||||
totalSeconds: Int,
|
||||
remainingSeconds: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val target =
|
||||
if (totalSeconds <= 0) 0f else (remainingSeconds.toFloat() / totalSeconds).coerceIn(0f, 1f)
|
||||
val progress: Float by animateFloatAsState(
|
||||
targetValue = target,
|
||||
label = "Timer",
|
||||
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(progress)
|
||||
.background(
|
||||
color = Purple,
|
||||
shape = RoundedCornerShape(percent = 50),
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "$remainingSeconds",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 8.dp),
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.Grey
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.R
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR
|
||||
|
||||
@Composable
|
||||
fun Toolbar(
|
||||
modifier: Modifier = Modifier,
|
||||
currentQuestionIndex: Int = 0,
|
||||
totalQuestions: Int = 0,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "${currentQuestionIndex + 1}/$totalQuestions",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.background(
|
||||
color = Grey,
|
||||
shape = RoundedCornerShape(percent = 50),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.background(
|
||||
color = Grey,
|
||||
shape = RoundedCornerShape(percent = 50),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = DesignR.drawable.ic_type),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.quiz),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
@@ -12,4 +14,4 @@ fun NavGraphBuilder.quizScreen() {
|
||||
composable<QuizRoute> {
|
||||
QuizScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,16 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.utils
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
sealed interface Result<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>
|
||||
data class Error(val exception: Throwable) : Result<Nothing>
|
||||
data object Loading : Result<Nothing>
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.asResult(): Flow<Result<T>> = map<T, Result<T>> { Result.Success(it) }
|
||||
.onStart { emit(Result.Loading) }
|
||||
.catch { emit(Result.Error(it)) }
|
7
ui/quiz/src/main/res/values/strings.xml
Normal file
7
ui/quiz/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="quiz">Quiz</string>
|
||||
<string name="continue_text">Continue</string>
|
||||
<string name="correct">Correct</string>
|
||||
<string name="wrong">Wrong</string>
|
||||
</resources>
|
Reference in New Issue
Block a user