diff --git a/.gitignore b/.gitignore index 427a604..4434be0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store -/build +build/ /captures .externalNativeBuild .cxx @@ -16,4 +16,5 @@ local.properties # Project exclude paths /build-logic/convention/build/ -/build-logic/convention/build/classes/kotlin/main/ \ No newline at end of file +/build-logic/convention/build/classes/kotlin/main/ +/.idea/ diff --git a/Kahoot__-_App_Developer_Challenge.pdf b/Kahoot__-_App_Developer_Challenge.pdf new file mode 100644 index 0000000..3ded219 Binary files /dev/null and b/Kahoot__-_App_Developer_Challenge.pdf differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e916be3..7afebdf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,8 +27,10 @@ android { } dependencies { + implementation(projects.ui.quiz) + + implementation(projects.model.data) + implementation(libs.androidx.activity.compose) - implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.hilt.navigation.compose) - implementation(libs.app.update.ktx) } \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 0000000..f6231a0 --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(libs.plugins.kahootquiz.android.library) + alias(libs.plugins.kahootquiz.android.library.hilt) +} + +android { + namespace = "dev.adriankuta.kahootquiz.core.network" + + defaultConfig { + buildConfigField("String", "KAHOOT_BASE_URL", "\"https://create.kahoot.it\"") + } + + buildFeatures { + buildConfig = true + } +} + +dependencies { + // Gson for JSON serialization/deserialization + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) +} diff --git a/core/network/config/detekt/detekt.yml b/core/network/config/detekt/detekt.yml new file mode 100644 index 0000000..809b757 --- /dev/null +++ b/core/network/config/detekt/detekt.yml @@ -0,0 +1,10 @@ +# Deviations from defaults +formatting: + TrailingCommaOnCallSite: + active: true + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: true + autoCorrect: true + useTrailingCommaOnDeclarationSite: true \ No newline at end of file diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/QuizService.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/QuizService.kt new file mode 100644 index 0000000..df6679d --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/QuizService.kt @@ -0,0 +1,4 @@ +package dev.adriankuta.kahootquiz.core.network + +interface QuizService { +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/QuizServiceImpl.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/QuizServiceImpl.kt new file mode 100644 index 0000000..38d4cbd --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/QuizServiceImpl.kt @@ -0,0 +1,4 @@ +package dev.adriankuta.kahootquiz.core.network + +internal class QuizServiceImpl: QuizService { +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/di/NetworkModule.kt new file mode 100644 index 0000000..17bdb92 --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/di/NetworkModule.kt @@ -0,0 +1,29 @@ +package dev.adriankuta.kahootquiz.core.network.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dev.adriankuta.kahootquiz.core.network.BuildConfig +import dev.adriankuta.kahootquiz.core.network.retrofit.QuizApi +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun providesRetrofit(): Retrofit = + Retrofit.Builder() + .baseUrl(BuildConfig.KAHOOT_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + @Provides + @Singleton + fun providesQuizApi(retrofit: Retrofit): QuizApi = + retrofit.create(QuizApi::class.java) +} diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/CommonDtos.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/CommonDtos.kt new file mode 100644 index 0000000..f5bd507 --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/CommonDtos.kt @@ -0,0 +1,20 @@ +package dev.adriankuta.kahootquiz.core.network.model + +// Commonly reused DTOs + +data class LanguageInfoDto( + val language: String?, + val lastUpdatedOn: Long?, + val readAloudSupported: Boolean? +) + +// Minimal channel info + +data class ChannelDto(val id: String?) + +// Geometry helpers + +data class PointDto( + val x: Int?, + val y: Int? +) diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/ContentTagsDto.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/ContentTagsDto.kt new file mode 100644 index 0000000..27bc9ea --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/ContentTagsDto.kt @@ -0,0 +1,8 @@ +package dev.adriankuta.kahootquiz.core.network.model + +// Content tags DTOs + +data class ContentTagsDto( + val curriculumCodes: List?, + val generatedCurriculumCodes: List? +) diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/CoverDtos.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/CoverDtos.kt new file mode 100644 index 0000000..6192bb2 --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/CoverDtos.kt @@ -0,0 +1,32 @@ +package dev.adriankuta.kahootquiz.core.network.model + +// Cover metadata and related DTOs + +data class CoverMetadataDto( + val id: String?, + val altText: String?, + val contentType: String?, + val origin: String?, + val externalRef: String?, + val resources: String?, + val width: Int?, + val height: Int?, + val extractedColors: List?, + val blurhash: String?, + val crop: CropDto? +) + +// Color extracted from cover image + +data class ExtractedColorDto( + val swatch: String?, + val rgbHex: String? +) + +// Crop descriptor + +data class CropDto( + val origin: PointDto?, + val target: PointDto?, + val circular: Boolean? +) diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/MetadataDtos.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/MetadataDtos.kt new file mode 100644 index 0000000..16829fc --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/MetadataDtos.kt @@ -0,0 +1,42 @@ +package dev.adriankuta.kahootquiz.core.network.model + +// Metadata section DTOs + +data class MetadataDto( + val access: AccessDto?, + val duplicationProtection: Boolean?, + val featuredListMemberships: List?, + val lastEdit: LastEditDto?, + val versionMetadata: VersionMetadataDto? +) + +// Access settings + +data class AccessDto( + val groupRead: List?, + val folderGroupIds: List?, + val features: List? +) + +// Featured list membership + +data class FeaturedListMembershipDto( + val list: String?, + val addedAt: Long? +) + +// Last edit information + +data class LastEditDto( + val editorUserId: String?, + val editorUsername: String?, + val editTimestamp: Long? +) + +// Version metadata + +data class VersionMetadataDto( + val version: Int?, + val created: Long?, + val creator: String? +) diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuestionDtos.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuestionDtos.kt new file mode 100644 index 0000000..9adcab2 --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuestionDtos.kt @@ -0,0 +1,81 @@ +package dev.adriankuta.kahootquiz.core.network.model + +// Question and choice related DTOs + +data class QuestionDto( + val type: String?, + val question: String?, + val time: Int?, + val points: Boolean?, + val pointsMultiplier: Int?, + val choices: List?, + val layout: String?, + val image: String?, + val imageMetadata: ImageMetadataDto?, + val resources: String?, + val video: VideoDto?, + val questionFormat: Int?, + val languageInfo: LanguageInfoDto?, + val media: List?, + val choiceRange: ChoiceRangeDto? +) + +// Choice option + +data class ChoiceDto( + val answer: String?, + val correct: Boolean?, + val languageInfo: LanguageInfoDto? +) + +// Optional video attachment + +data class VideoDto( + val id: String? = null, + val startTime: Int?, + val endTime: Int?, + val service: String?, + val fullUrl: String? +) + +// Image metadata appearing in multiple places + +data class ImageMetadataDto( + val id: String?, + val altText: String? = null, + val contentType: String?, + val origin: String? = null, + val externalRef: String? = null, + val resources: String? = null, + val width: Int? = null, + val height: Int? = null, + val effects: List? = null, + val crop: CropDto? = null +) + +// Generic media item on question + +data class MediaItemDto( + val type: String?, + val zIndex: Int?, + val isColorOnly: Boolean?, + val id: String?, + val altText: String? = null, + val contentType: String?, + val origin: String? = null, + val externalRef: String? = null, + val resources: String? = null, + val width: Int? = null, + val height: Int? = null, + val crop: CropDto? = null +) + +// Slider range for "slider" question type + +data class ChoiceRangeDto( + val start: Int?, + val end: Int?, + val step: Int?, + val correct: Int?, + val tolerance: Int? +) diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuizResponse.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuizResponse.kt new file mode 100644 index 0000000..7001c97 --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuizResponse.kt @@ -0,0 +1,10 @@ +package dev.adriankuta.kahootquiz.core.network.model + +// This file used to contain all DTOs in one place. +// The DTOs have been split into separate files for maintainability: +// - QuizResponseDto.kt +// - CommonDtos.kt +// - CoverDtos.kt +// - QuestionDtos.kt +// - MetadataDtos.kt +// Keeping this file as a placeholder to avoid breaking any imports by file path (package remains the same). diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuizResponseDto.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuizResponseDto.kt new file mode 100644 index 0000000..0c1c54b --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/model/QuizResponseDto.kt @@ -0,0 +1,36 @@ +package dev.adriankuta.kahootquiz.core.network.model + +import com.google.gson.annotations.SerializedName + +// Root response for a Kahoot quiz details (network layer DTO) +data class QuizResponseDto( + val uuid: String?, + val language: String?, + val creator: String?, + @SerializedName("creator_username") val creatorUsername: String?, + val compatibilityLevel: Int?, + @SerializedName("creator_primary_usage") val creatorPrimaryUsage: String?, + val folderId: String?, + val themeId: String?, + val visibility: Int?, + val audience: String?, + val title: String?, + val description: String?, + val quizType: String?, + val cover: String?, + val coverMetadata: CoverMetadataDto?, + val questions: List?, + val contentTags: ContentTagsDto?, + val metadata: MetadataDto?, + val resources: String?, + val slug: String?, + val languageInfo: LanguageInfoDto?, + val inventoryItemIds: List?, + val channels: List?, + val isValid: Boolean?, + val playAsGuest: Boolean?, + val hasRestrictedContent: Boolean?, + val type: String?, + val created: Long?, + val modified: Long? +) diff --git a/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/retrofit/QuizApi.kt b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/retrofit/QuizApi.kt new file mode 100644 index 0000000..8d1be03 --- /dev/null +++ b/core/network/src/main/kotlin/dev/adriankuta/kahootquiz/core/network/retrofit/QuizApi.kt @@ -0,0 +1,10 @@ +package dev.adriankuta.kahootquiz.core.network.retrofit + +import dev.adriankuta.kahootquiz.core.network.model.QuizResponseDto +import retrofit2.http.GET + +interface QuizApi { + + @GET("/rest/kahoots/fb4054fc-6a71-463e-88cd-243876715bc1") + suspend fun getQuiz(): QuizResponseDto +} diff --git a/core/network/src/test/kotlin/dev/adriankuta/kahootquiz/core/network/QuizResponseDtoParsingTest.kt b/core/network/src/test/kotlin/dev/adriankuta/kahootquiz/core/network/QuizResponseDtoParsingTest.kt new file mode 100644 index 0000000..372cd26 --- /dev/null +++ b/core/network/src/test/kotlin/dev/adriankuta/kahootquiz/core/network/QuizResponseDtoParsingTest.kt @@ -0,0 +1,100 @@ +package dev.adriankuta.kahootquiz.core.network + +import com.google.common.truth.Truth.assertThat +import com.google.gson.Gson +import dev.adriankuta.kahootquiz.core.network.model.QuizResponseDto +import org.junit.Test +import java.io.InputStreamReader + +class QuizResponseDtoParsingTest { + + private fun loadSample(): QuizResponseDto { + val stream = checkNotNull(javaClass.classLoader?.getResourceAsStream("sample_quiz.json")) { + "sample_quiz.json not found on test classpath" + } + stream.use { input -> + return Gson().fromJson(InputStreamReader(input), QuizResponseDto::class.java) + } + } + + @Test + fun parses_root_fields_correctly() { + val dto = loadSample() + + assertThat(dto.uuid).isEqualTo("fb4054fc-6a71-463e-88cd-243876715bc1") + assertThat(dto.title).isEqualTo("Seven Wonders of the Ancient World") + assertThat(dto.creatorUsername).isEqualTo("KahootStudio") + assertThat(dto.creatorPrimaryUsage).isEqualTo("teacher") + assertThat(dto.quizType).isEqualTo("quiz") + assertThat(dto.isValid).isTrue() + assertThat(dto.playAsGuest).isTrue() + assertThat(dto.hasRestrictedContent).isFalse() + assertThat(dto.created).isGreaterThan(0) + assertThat(dto.modified).isGreaterThan(0) + } + + @Test + fun parses_cover_metadata_and_colors() { + val dto = loadSample() + val cover = checkNotNull(dto.coverMetadata) + assertThat(cover.id).isEqualTo("0b64142f-0624-4014-9f50-b65e6be93d8f") + assertThat(cover.contentType).isEqualTo("image/jpeg") + assertThat(cover.extractedColors).isNotNull() + assertThat(cover.extractedColors).hasSize(4) + assertThat(cover.extractedColors?.first()?.swatch).isEqualTo("VIBRANT") + assertThat(cover.blurhash).isNotNull() + val crop = checkNotNull(cover.crop) + assertThat(crop.circular).isFalse() + assertThat(crop.origin?.x).isEqualTo(227) + assertThat(crop.target?.y).isEqualTo(1299) + } + + @Test + fun parses_questions_and_choices() { + val dto = loadSample() + val questions = checkNotNull(dto.questions) + assertThat(questions).hasSize(12) + + // First question true/false + val q1 = questions[0] + assertThat(q1.type).isEqualTo("quiz") + assertThat(q1.layout).isEqualTo("TRUE_FALSE") + assertThat(q1.choices).hasSize(2) + assertThat(q1.choices?.get(0)?.answer).isEqualTo("True") + assertThat(q1.choices?.get(0)?.correct).isTrue() + assertThat(q1.choices?.get(1)?.answer).isEqualTo("False") + assertThat(q1.choices?.get(1)?.correct).isFalse() + + // Open ended question exists and has accepted answers + val openEnded = questions.first { it.type == "open_ended" } + assertThat(openEnded.choices).isNotNull() + val answers = openEnded.choices!!.map { it.answer } + assertThat(answers).containsAtLeast("Helios", "helios") + + // Slider question has choiceRange + val slider = questions.first { it.type == "slider" } + val range = checkNotNull(slider.choiceRange) + assertThat(range.start).isEqualTo(0) + assertThat(range.end).isEqualTo(7) + assertThat(range.step).isEqualTo(1) + assertThat(range.correct).isEqualTo(1) + assertThat(range.tolerance).isEqualTo(0) + } + + @Test + fun parses_metadata_and_channels() { + val dto = loadSample() + val metadata = checkNotNull(dto.metadata) + assertThat(metadata.duplicationProtection).isTrue() + assertThat(metadata.featuredListMemberships).isNotNull() + assertThat(metadata.featuredListMemberships).isNotEmpty() + assertThat(metadata.versionMetadata?.version).isEqualTo(4) + assertThat(metadata.lastEdit?.editorUsername).isEqualTo("KahootStudio") + + val channels = checkNotNull(dto.channels) + assertThat(channels).hasSize(1) + assertThat(channels.first().id).isEqualTo("247c3eb4-af80-4c1f-b006-558682c7bd2f") + + assertThat(dto.languageInfo?.readAloudSupported).isTrue() + } +} diff --git a/core/network/src/test/resources/sample_quiz.json b/core/network/src/test/resources/sample_quiz.json new file mode 100644 index 0000000..aa8e613 --- /dev/null +++ b/core/network/src/test/resources/sample_quiz.json @@ -0,0 +1,404 @@ +{ + "uuid": "fb4054fc-6a71-463e-88cd-243876715bc1", + "language": "English", + "creator": "4c1574ee-de54-40a2-be15-8d72b333afad", + "creator_username": "KahootStudio", + "compatibilityLevel": 27, + "creator_primary_usage": "teacher", + "folderId": "20915e7a-34d5-458b-91e9-2ed290484438", + "themeId": "ace311f1-2de4-450b-ba7d-d8a7a81705a5", + "visibility": 1, + "audience": "Social", + "title": "Seven Wonders of the Ancient World", + "description": "A geography quiz about the Seven Wonders of the Ancient World. See how much you know about these ancient buildings and monuments!\n#trivia #history #geography #sevenwonders", + "quizType": "quiz", + "cover": "https://media.kahoot.it/0b64142f-0624-4014-9f50-b65e6be93d8f", + "coverMetadata": { + "id": "0b64142f-0624-4014-9f50-b65e6be93d8f", + "altText": "The Pyramids, Giza, Egypt", + "contentType": "image/jpeg", + "origin": "Getty Images", + "externalRef": "1360795720", + "resources": "Nick Brundle Photography/Moment/Getty Images", + "width": 2309, + "height": 1299, + "extractedColors": [ + { + "swatch": "VIBRANT", + "rgbHex": "#28a8d8" + }, + { + "swatch": "LIGHT_VIBRANT", + "rgbHex": "#88c8e0" + }, + { + "swatch": "DARK_VIBRANT", + "rgbHex": "#307890" + }, + { + "swatch": "LIGHT_MUTED", + "rgbHex": "#d0d0c0" + } + ], + "blurhash": "UuJ*#Qxtx]xaCAj[W=WqEma}M{R*M|WVn#j?", + "crop": { + "origin": {"x": 227, "y": 0}, + "target": {"x": 1948, "y": 1299}, + "circular": false + } + }, + "questions": [ + { + "type": "quiz", + "question": "\u003Cb\u003ETrue or false: \u003C/b\u003EThe list of seven wonders is based on ancient Greek guidebooks for tourists.", + "time": 20000, + "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}} + ], + "layout": "TRUE_FALSE", + "image": "https://media.kahoot.it/b2709905-1c6e-45a0-9cc1-34c6580495e5", + "imageMetadata": { + "id": "b2709905-1c6e-45a0-9cc1-34c6580495e5", + "altText": "Old engraved illustration of bird's eye View of Alexandria, Egypt", + "contentType": "image/jpeg", + "origin": "Getty Images", + "externalRef": "1352146635", + "resources": "mikroman6/Moment/Getty Images", + "width": 2133, + "height": 1406 + }, + "resources": "mikroman6/Moment/Getty Images", + "video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "The Great Pyramid of Giza is the oldest of the wonders. What was its purpose?", + "time": 30000, + "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}} + ], + "resources": "", + "video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [ + { + "type": "background_image", + "zIndex": -1, + "isColorOnly": false, + "id": "0b64142f-0624-4014-9f50-b65e6be93d8f", + "altText": "The Pyramids, Giza, Egypt", + "contentType": "image/jpeg", + "origin": "Getty Images", + "externalRef": "1360795720", + "resources": "Nick Brundle Photography/Moment/Getty Images", + "width": 2309, + "height": 1299, + "crop": {"origin": {"x": 227, "y": 0}, "target": {"x": 1948, "y": 1299}, "circular": false} + } + ] + }, + { + "type": "quiz", + "question": "Why were the Hanging Gardens of Babylon supposedly built?", + "time": 30000, + "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}} + ], + "image": "https://media.kahoot.it/7bce7efb-3d94-495c-905f-9c14190b7910", + "imageMetadata": { + "id": "7bce7efb-3d94-495c-905f-9c14190b7910", + "contentType": "image/*", + "resources": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Hanging_Gardens_of_Babylon.jpg CC0", + "effects": [] + }, + "resources": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Hanging_Gardens_of_Babylon.jpg CC0", + "video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "The Temple of Artemis was located in the city Ephesus. It's the territory of _____ today.", + "time": 30000, + "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}} + ], + "image": "https://media.kahoot.it/f999f2a2-5450-4821-a3c8-94288720bd46", + "imageMetadata": { + "id": "f999f2a2-5450-4821-a3c8-94288720bd46", + "contentType": "image/*", + "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", + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "The second Temple of Artemis was burnt down by Herostratus. Why did he set the temple on fire?", + "time": 30000, + "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}} + ], + "image": "https://media.kahoot.it/fe2c5c06-6d2e-4a5a-9441-a9c77391130e_opt", + "imageMetadata": { + "id": "fe2c5c06-6d2e-4a5a-9441-a9c77391130e", + "contentType": "image/*", + "resources": " [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a9/Temple_of_Artemis.jpg", + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "The Statue of Zeus was built by the sculptor Phidias. Where was it located?", + "time": 30000, + "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}} + ], + "image": "https://media.kahoot.it/9074a275-1874-4cb9-9c9f-248173ceae9d", + "imageMetadata": { + "id": "9074a275-1874-4cb9-9c9f-248173ceae9d", + "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} + }, + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "The Mausoleum at Halicarnassus was a tomb for a Persian governor. Who was buried there?", + "time": 20000, + "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}} + ], + "image": "https://media.kahoot.it/38f43ef3-4507-4f11-ae33-f3e833a47d19", + "imageMetadata": { + "id": "38f43ef3-4507-4f11-ae33-f3e833a47d19", + "altText": "Yellow Question Mark on Purple Background, Paper Craft", + "contentType": "image/jpeg", + "origin": "Getty Images", + "externalRef": "1501192535", + "resources": "MirageC/Moment/Getty Images", + "width": 2120, + "height": 1414 + }, + "resources": "MirageC/Moment/Getty Images", + "video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "open_ended", + "question": "The Colossus of Rhodes was 108 feet (33 metres) high. Which God was it based on?", + "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}} + ], + "image": "https://media.kahoot.it/d4ccbf4e-1026-46ad-ab35-84dc17c4d3a0_opt", + "imageMetadata": { + "id": "d4ccbf4e-1026-46ad-ab35-84dc17c4d3a0", + "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} + }, + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "The Lighthouse of Alexandria was destroyed in the 14th century. How?", + "time": 30000, + "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}} + ], + "image": "https://media.kahoot.it/e2d22765-942b-4dbd-9fd6-d71142d775c3", + "imageMetadata": { + "id": "e2d22765-942b-4dbd-9fd6-d71142d775c3", + "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} + }, + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "Which of the seven wonders was the tallest?", + "time": 30000, + "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}} + ], + "image": "https://media.kahoot.it/19382163-196f-495d-9a84-c2d8c3fd716c", + "imageMetadata": { + "id": "19382163-196f-495d-9a84-c2d8c3fd716c", + "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} + }, + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "slider", + "question": "How many of the Seven Wonders still exist?", + "time": 20000, + "pointsMultiplier": 2, + "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", + "contentType": "image/*", + "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", + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + }, + { + "type": "quiz", + "question": "Which of the Seven Wonders is the only one that still exists?", + "time": 30000, + "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}} + ], + "image": "https://media.kahoot.it/34b01038-031c-4d23-b8a0-55402916586f_opt", + "imageMetadata": { + "id": "34b01038-031c-4d23-b8a0-55402916586f", + "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} + }, + "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": ""}, + "questionFormat": 0, + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "media": [] + } + ], + "contentTags": {"curriculumCodes": [], "generatedCurriculumCodes": []}, + "metadata": { + "access": { + "groupRead": [ + "b5c71d39-c229-4eeb-8648-cd8518ec068a", + "99159f29-7004-460b-a87a-cd4aab39ba4c", + "4597f49e-8b3b-40a5-985a-e6da01761947", + "ff2abab7-29f9-4669-8085-66e1089045a0", + "4caead57-3dd8-41c0-8f60-c5da69881b8e", + "36022fd9-43e1-4b36-9c98-a6a3b2b53038" + ], + "folderGroupIds": [], + "features": ["PremiumEduContent"] + }, + "duplicationProtection": true, + "featuredListMemberships": [ + {"list": "youngfeatured", "addedAt": 1682336780289}, + {"list": "featured", "addedAt": 1682336738189} + ], + "lastEdit": { + "editorUserId": "4c1574ee-de54-40a2-be15-8d72b333afad", + "editorUsername": "KahootStudio", + "editTimestamp": 1727277651459 + }, + "versionMetadata": { + "version": 4, + "created": 1727277651160, + "creator": "4c1574ee-de54-40a2-be15-8d72b333afad" + } + }, + "resources": "Nick Brundle Photography/Moment/Getty Images", + "slug": "seven-wonders-of-the-ancient-world", + "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}, + "inventoryItemIds": [], + "channels": [{"id": "247c3eb4-af80-4c1f-b006-558682c7bd2f"}], + "isValid": true, + "playAsGuest": true, + "hasRestrictedContent": false, + "type": "quiz", + "created": 1527169083018, + "modified": 1754523196463 +} \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts new file mode 100644 index 0000000..be201de --- /dev/null +++ b/domain/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.kahootquiz.android.library) + alias(libs.plugins.kahootquiz.android.library.hilt) +} + +android { + namespace = "dev.adriankuta.kahootquiz.domain" +} + +dependencies { +} diff --git a/domain/config/detekt/detekt.yml b/domain/config/detekt/detekt.yml new file mode 100644 index 0000000..809b757 --- /dev/null +++ b/domain/config/detekt/detekt.yml @@ -0,0 +1,10 @@ +# Deviations from defaults +formatting: + TrailingCommaOnCallSite: + active: true + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: true + autoCorrect: true + useTrailingCommaOnDeclarationSite: true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4339f08..242de8d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +retrofit = "3.0.0" targetSdk = "36" compileSdk = "36" minSdk = "23" @@ -115,6 +116,8 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- material = { group = "com.google.android.material", name = "material", version.ref = "material" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } play-services-ads = { module = "com.google.android.gms:play-services-ads", version.ref = "playServicesAds" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "reviewKtx" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } diff --git a/model/data/build.gradle.kts b/model/data/build.gradle.kts new file mode 100644 index 0000000..09eaf63 --- /dev/null +++ b/model/data/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.kahootquiz.android.library) + alias(libs.plugins.kahootquiz.android.library.hilt) +} + +android { + namespace = "dev.adriankuta.kahootquiz.model.data" +} + +dependencies { + implementation(projects.core.network) + implementation(projects.domain) +} diff --git a/model/data/config/detekt/detekt.yml b/model/data/config/detekt/detekt.yml new file mode 100644 index 0000000..809b757 --- /dev/null +++ b/model/data/config/detekt/detekt.yml @@ -0,0 +1,10 @@ +# Deviations from defaults +formatting: + TrailingCommaOnCallSite: + active: true + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: true + autoCorrect: true + useTrailingCommaOnDeclarationSite: true \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index febf40f..6a10fb0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,3 +25,7 @@ rootProject.name = "KahootQuiz" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") +include(":core:network") +include(":domain") +include(":model:data") +include(":ui:quiz") diff --git a/ui/config/detekt/detekt.yml b/ui/config/detekt/detekt.yml new file mode 100644 index 0000000..809b757 --- /dev/null +++ b/ui/config/detekt/detekt.yml @@ -0,0 +1,10 @@ +# Deviations from defaults +formatting: + TrailingCommaOnCallSite: + active: true + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: true + autoCorrect: true + useTrailingCommaOnDeclarationSite: true \ No newline at end of file diff --git a/ui/quiz/build.gradle.kts b/ui/quiz/build.gradle.kts new file mode 100644 index 0000000..20f0afc --- /dev/null +++ b/ui/quiz/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.kahootquiz.android.library.compose) + alias(libs.plugins.kahootquiz.android.library.hilt) +} + +android { + namespace = "dev.adriankuta.kahootquiz.ui.quiz" +} + +dependencies { + implementation(projects.domain) +} diff --git a/ui/quiz/config/detekt/detekt.yml b/ui/quiz/config/detekt/detekt.yml new file mode 100644 index 0000000..809b757 --- /dev/null +++ b/ui/quiz/config/detekt/detekt.yml @@ -0,0 +1,10 @@ +# Deviations from defaults +formatting: + TrailingCommaOnCallSite: + active: true + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: true + autoCorrect: true + useTrailingCommaOnDeclarationSite: true \ No newline at end of file