From 70940c18318aaf06b19a281956a051348cc2c97f Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 21:48:06 +0300 Subject: [PATCH 01/10] Fix the progress bar on light theme --- .../kotlin/ui/screen/lesson/composable/LessonProgressBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonProgressBar.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonProgressBar.kt index d0eb0a2..0180ea2 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonProgressBar.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonProgressBar.kt @@ -33,7 +33,7 @@ fun LessonProgressBar( ProgressBarLine(width = progressBarWidth, color = Gray) // foreground (progress) val progressWidth = (progressBarWidth * progressPercent).coerceAtMost(progressBarWidth) - ProgressBarLine(width = progressWidth, color = MaterialTheme.colors.primary) + ProgressBarLine(width = progressWidth, color = MaterialTheme.colors.secondary) } } From a069f2d000502ce74354ae00c93731d744af3e48 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 21:58:12 +0300 Subject: [PATCH 02/10] Add "Continue" after answering a question --- .../ui/screen/lesson/LessonViewModel.kt | 14 ++++++---- .../lesson/composable/LessonAutoScroll.kt | 2 +- .../handler/OnChoiceClickEventHandler.kt | 4 +-- .../handler/QuestionViewEventHandler.kt | 12 ++++---- .../screen/lesson/mapper/LessonTreeManager.kt | 2 +- .../lesson/mapper/LessonViewStateMapper.kt | 28 +++++++++++++------ 6 files changed, 37 insertions(+), 25 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt index 40fd26a..597fe3c 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt @@ -73,16 +73,18 @@ class LessonViewModel( @optics data class LocalState( - val answers: Map>, - val openAnswers: Map, + val selectedAnswers: Map>, + val openAnswersInput: Map, + val chosen: Map, + val answered: Set, val completed: Set, - val choices: Map, ) { companion object { val Initial = LocalState( - answers = emptyMap(), - openAnswers = emptyMap(), - choices = emptyMap(), + selectedAnswers = emptyMap(), + openAnswersInput = emptyMap(), + chosen = emptyMap(), + answered = emptySet(), completed = emptySet(), ) } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt index 9664d8d..9507874 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt @@ -23,7 +23,7 @@ fun AutoScrollEffect( ) { val itemsCount = items.size LaunchedEffect(itemsCount) { - if (itemsCount >= 2 && !items[itemsCount - 2].isQuestion()) { + if (itemsCount >= 2) { // ensure auto scrolls works for images that are loading repeat(4) { listState.animateScrollToItem(items.lastIndex) diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnChoiceClickEventHandler.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnChoiceClickEventHandler.kt index 217af5d..1a9add7 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnChoiceClickEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnChoiceClickEventHandler.kt @@ -8,7 +8,7 @@ import ui.EventHandler import ui.screen.lesson.LessonViewEvent import ui.screen.lesson.LessonViewModel.LocalState import ui.screen.lesson.LessonVmContext -import ui.screen.lesson.choices +import ui.screen.lesson.chosen import ui.screen.lesson.completed import ui.screen.lesson.mapper.toDomain @@ -21,7 +21,7 @@ class OnChoiceClickEventHandler( modifyState { state -> val questionId = event.questionId.toDomain() state.copy { - LocalState.choices transform { it + (questionId to ChoiceOptionId(event.choiceId)) } + LocalState.chosen transform { it + (questionId to ChoiceOptionId(event.choiceId)) } LocalState.completed transform { it + questionId } } } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionViewEventHandler.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionViewEventHandler.kt index 9cbebfa..7d8386b 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionViewEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionViewEventHandler.kt @@ -11,9 +11,9 @@ import ui.screen.lesson.LessonVmContext import ui.screen.lesson.QuestionTypeViewState.MultipleChoice import ui.screen.lesson.QuestionTypeViewState.SingleChoice import ui.screen.lesson.QuestionViewEvent -import ui.screen.lesson.answers -import ui.screen.lesson.completed +import ui.screen.lesson.answered import ui.screen.lesson.mapper.toDomain +import ui.screen.lesson.selectedAnswers class QuestionViewEventHandler( private val platform: Platform, @@ -37,9 +37,9 @@ class QuestionViewEventHandler( modifyState { state -> val questionId = event.questionId.toDomain() val answerId = AnswerId(event.answerId) - if (questionId in state.answers) { + if (questionId in state.selectedAnswers) { // Already answered, modify the existing answer - LocalState.answers.index(Index.map(), questionId).modify(state) { + LocalState.selectedAnswers.index(Index.map(), questionId).modify(state) { when (event.questionType) { SingleChoice -> if (event.checked) { setOf(answerId) @@ -57,7 +57,7 @@ class QuestionViewEventHandler( } } else { // Never answered - LocalState.answers.modify(state) { answers -> + LocalState.selectedAnswers.modify(state) { answers -> if (event.checked) { answers + Pair(questionId, setOf(answerId)) } else { @@ -70,7 +70,7 @@ class QuestionViewEventHandler( private fun LessonVmContext.handleCheckClick(event: QuestionViewEvent.OnCheckClick) { modifyState { state -> - LocalState.completed.modify(state) { completed -> + LocalState.answered.modify(state) { completed -> completed + event.questionId.toDomain() } } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonTreeManager.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonTreeManager.kt index 1c87d69..cefaaa4 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonTreeManager.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonTreeManager.kt @@ -58,7 +58,7 @@ class LessonTreeManager { localState: LessonViewModel.LocalState ): LessonItemId? = when (this) { is LinearItem -> next - is ChoiceItem -> localState.choices[id]?.let { choiceId -> + is ChoiceItem -> localState.chosen[id]?.let { choiceId -> options.firstOrNull { it.id == choiceId }?.next } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonViewStateMapper.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonViewStateMapper.kt index 77aad84..da80336 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonViewStateMapper.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/mapper/LessonViewStateMapper.kt @@ -25,22 +25,32 @@ class LessonViewStateMapper( it.toViewState(localState, content.items) }.toImmutableList(), cta = when (val currentItem = lessonItems.lastOrNull()) { - null, is QuestionItem, is OpenQuestionItem, + null, is OpenQuestionItem, is ChoiceItem -> null - else -> { - platform.log(LogLevel.Debug, "Current item: $currentItem") - if (currentItem is LinearItem && currentItem.next == null) { - CtaViewState.Finish(currentItem.id.toViewState()) + is QuestionItem -> { + if (currentItem.id in localState.answered) { + ctaViewState(currentItem) } else { - CtaViewState.Continue(currentItem.id.toViewState()) + null } } + + else -> ctaViewState(currentItem) }, progress = toProgressViewState(lessonItems) ) } + private fun ctaViewState(currentItem: LessonItem): CtaViewState { + platform.log(LogLevel.Debug, "Current item: $currentItem") + return if (currentItem is LinearItem && currentItem.next == null) { + CtaViewState.Finish(currentItem.id.toViewState()) + } else { + CtaViewState.Continue(currentItem.id.toViewState()) + } + } + private fun Lesson.toProgressViewState( lessonItems: List, ): LessonProgressViewState { @@ -119,7 +129,7 @@ class LessonViewStateMapper( ): OpenQuestionItemViewState = OpenQuestionItemViewState( id = id.toViewState(), question = question, - answer = localState.openAnswers[id], + answer = localState.openAnswersInput[id], correctAnswer = correctAnswer, answered = id in localState.completed, ) @@ -133,7 +143,7 @@ class LessonViewStateMapper( type = if (correct.size == 1) QuestionTypeViewState.SingleChoice else QuestionTypeViewState.MultipleChoice, answers = answers.map { it.toViewState(this, localState) } .toImmutableList(), - answered = id in localState.completed, + answered = id in localState.answered, ) private fun Answer.toViewState( @@ -144,7 +154,7 @@ class LessonViewStateMapper( text = text, explanation = explanation, correct = id in question.correct, - selected = id in localState.answers.getOrElse(question.id) { emptySet() }, + selected = id in localState.selectedAnswers.getOrElse(question.id) { emptySet() }, ) private fun TextItem.toViewState(): TextItemViewState = TextItemViewState( From 091e36022476483de7ef8f8456540e8b175b03ac Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 22:06:24 +0300 Subject: [PATCH 03/10] Fix image sizes --- .../screen/lesson/composable/item/ImageLessonItem.kt | 11 ++++++++++- .../composable/item/LottieAnimationLessonItem.kt | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/ImageLessonItem.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/ImageLessonItem.kt index 30597d3..8f12086 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/ImageLessonItem.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/ImageLessonItem.kt @@ -6,12 +6,21 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import component.ScreenType.* +import component.screenType import io.kamel.image.KamelImage import io.kamel.image.asyncPainterResource import ui.screen.lesson.ImageItemViewState import ui.screen.lesson.composable.ItemSpacingMedium +@Composable +fun imageSize(): Dp = when (screenType()) { + Mobile, Tablet -> 232.dp + Desktop -> 284.dp +} + @Composable fun ImageLessonItem( viewState: ImageItemViewState, @@ -20,7 +29,7 @@ fun ImageLessonItem( KamelImage( modifier = modifier .padding(top = ItemSpacingMedium) - .size(304.dp) + .size(imageSize()) .clip(RoundedCornerShape(16.dp)), resource = asyncPainterResource(viewState.imageUrl), contentDescription = null diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/LottieAnimationLessonItem.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/LottieAnimationLessonItem.kt index fdb989d..c667d5a 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/LottieAnimationLessonItem.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/item/LottieAnimationLessonItem.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import component.RemoteLottieAnimation import ui.screen.lesson.LottieAnimationItemViewState import ui.screen.lesson.composable.ItemSpacing @@ -17,7 +16,7 @@ fun LottieAnimationLessonItem( RemoteLottieAnimation( modifier = modifier .padding(ItemSpacing) - .size(304.dp), + .size(imageSize()), animationUrl = viewState.lottieUrl, repeat = true, ) From e6ec5c621b70d6ed9b74f0828fe00ce0ba7b1998 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 22:24:33 +0300 Subject: [PATCH 04/10] Fix the lesson card --- .../kotlin/component/text/Subtitle.kt | 5 ++++ .../screen/course/composable/CourseContent.kt | 9 ++++-- .../ui/screen/course/composable/LessonCard.kt | 29 ++++++++++++++----- .../programming/ProgrammingFundamentals.kt | 2 +- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/component/text/Subtitle.kt b/composeApp/src/commonMain/kotlin/component/text/Subtitle.kt index 5dc7d7c..ddc134f 100644 --- a/composeApp/src/commonMain/kotlin/component/text/Subtitle.kt +++ b/composeApp/src/commonMain/kotlin/component/text/Subtitle.kt @@ -5,17 +5,22 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow @Composable fun SubTitle( text: String, modifier: Modifier = Modifier, fontWeight: FontWeight? = null, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Clip, ) { Text( modifier = modifier, text = text, style = MaterialTheme.typography.subtitle1, fontWeight = fontWeight, + maxLines = maxLines, + overflow = overflow, ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/screen/course/composable/CourseContent.kt b/composeApp/src/commonMain/kotlin/ui/screen/course/composable/CourseContent.kt index 00db598..67a7496 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/course/composable/CourseContent.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/course/composable/CourseContent.kt @@ -12,7 +12,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import component.BackButton import component.LearnScaffold -import component.platformHorizontalPadding +import component.ScreenType.* +import component.screenType import ui.screen.course.CourseItemViewState import ui.screen.course.CourseViewEvent import ui.screen.course.CourseViewState @@ -28,7 +29,11 @@ fun CourseContent( }), title = viewState.name ) { contentPadding -> - val horizontalPadding = platformHorizontalPadding() + val horizontalPadding = when (screenType()) { + Mobile -> 8.dp + Tablet -> 16.dp + Desktop -> 16.dp + } LazyColumn( modifier = Modifier .fillMaxSize() diff --git a/composeApp/src/commonMain/kotlin/ui/screen/course/composable/LessonCard.kt b/composeApp/src/commonMain/kotlin/ui/screen/course/composable/LessonCard.kt index c03d677..5e856ff 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/course/composable/LessonCard.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/course/composable/LessonCard.kt @@ -9,12 +9,16 @@ import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import component.ScreenType.* import component.isLargeScreen +import component.screenType import component.text.SubTitle import component.text.Title import io.kamel.image.KamelImage @@ -58,11 +62,11 @@ fun LessonCard( Column( modifier = Modifier .onSizeChanged { - height = minOf(with(density) { it.height.toDp() }, height) + height = maxOf(with(density) { it.height.toDp() }, height) } .defaultMinSize(minHeight = height) .padding( - start = if (isLargeScreen()) 24.dp else 20.dp, + start = if (isLargeScreen()) 24.dp else 16.dp, end = if (isLargeScreen()) 16.dp else 12.dp ) .padding(vertical = 16.dp), @@ -72,8 +76,12 @@ fun LessonCard( text = lesson.name, fontWeight = FontWeight.Bold ) - Spacer(Modifier.height(4.dp)) - SubTitle(text = lesson.tagline) + Spacer(Modifier.height(8.dp)) + SubTitle( + text = lesson.tagline, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) } } } @@ -86,10 +94,17 @@ private fun LessonImage( ) { KamelImage( modifier = modifier - .aspectRatio(1f), - contentScale = ContentScale.Crop, + .aspectRatio( + when (screenType()) { + Mobile -> 0.6f + Tablet -> 1f + Desktop -> 1f + } + ) + .clip(RoundedCornerShape(topStart = 24.dp, bottomStart = 24.dp)), + contentScale = ContentScale.FillHeight, contentAlignment = Alignment.CenterStart, resource = asyncPainterResource(imageUrl), - contentDescription = null + contentDescription = null, ) } \ No newline at end of file diff --git a/server/src/main/kotlin/ivy/learn/data/cms/course/programming/ProgrammingFundamentals.kt b/server/src/main/kotlin/ivy/learn/data/cms/course/programming/ProgrammingFundamentals.kt index ac88d55..fd4b308 100644 --- a/server/src/main/kotlin/ivy/learn/data/cms/course/programming/ProgrammingFundamentals.kt +++ b/server/src/main/kotlin/ivy/learn/data/cms/course/programming/ProgrammingFundamentals.kt @@ -10,7 +10,7 @@ object ProgrammingFundamentals : CourseDsl({ lesson( name = "Programming: Math in disguise", tagline = "Mathematical functions can do much more than you think." + - " Function Composition.", + " Function composition.", imageUrl = LessonImage ) }) \ No newline at end of file From 8167a82c713df9bc8c2e05d391b118404e5bb4a5 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 22:41:55 +0300 Subject: [PATCH 05/10] Print lesson story --- .../ProgrammingMathInDisguise.kt | 7 +++- .../kotlin/ivy/model/dsl/LearnCmsDsl.kt | 33 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt index b23cede..242303c 100644 --- a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt +++ b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt @@ -72,7 +72,11 @@ private fun LessonContentScope.meetHarry() { line("Harry is a gifted scientist who can solve complex problems in a simple and elegant way.") line("He's also quite impatient and tends to test his ideas at Friday on production.") } - style = TextStyle.Body + } + text("harry_hi2") { + text = textBuilder { + line("He's also quite impatient and tends to test his ideas at Friday on production.") + } } } @@ -579,4 +583,5 @@ private fun LessonContentScope.lessonSummary() { fun main() { val lesson = programmingMathInDisguise() printLessonJson(lesson) + println(story(lesson)) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/ivy/model/dsl/LearnCmsDsl.kt b/shared/src/commonMain/kotlin/ivy/model/dsl/LearnCmsDsl.kt index 972d38c..d308a3d 100644 --- a/shared/src/commonMain/kotlin/ivy/model/dsl/LearnCmsDsl.kt +++ b/shared/src/commonMain/kotlin/ivy/model/dsl/LearnCmsDsl.kt @@ -18,6 +18,33 @@ fun printLessonJson(lesson: LessonContent) { println(Json.encodeToString(lesson)) } +fun story( + lesson: LessonContent, + choices: Map = emptyMap(), + currentItemId: LessonItemId? = lesson.rootItem, +): String = if (currentItemId != null) { + when (val item = lesson.items[currentItemId]!!) { + is TextItem -> { + when (item.style) { + TextStyle.Heading -> "\n# ${item.text}\n\n" + else -> item.text + } + story(lesson, choices, item.next) + } + + is ChoiceItem -> { + val option = item.options.firstOrNull { + choices[currentItemId] == it.id + } ?: item.options.first() + story(lesson, choices, option.next) + } + + is LinearItem -> story(lesson, choices, item.next) + else -> "" + } +} else { + "" +} + private fun validateIdsExistence(lesson: LessonContent) { allItemsIds( currentId = lesson.rootItem, @@ -186,13 +213,11 @@ class TextBuilder : TextBuilderScope { } fun build(): String = buildString { - for ((index, item) in items.withIndex()) { + for (item in items) { when (item) { is Item.Line -> { append(item.text + "\n") - if (items.getOrNull(index + 1) !is Item.Bullet) { - append("\n") - } + append("\n") } is Item.NewLine -> append("\n") From c6e8d7ce4eeba1a57562986bffcc8036c5681806 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 23:00:04 +0300 Subject: [PATCH 06/10] WIP: Improve content --- .../ProgrammingMathInDisguise.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt index 242303c..dcd9ade 100644 --- a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt +++ b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt @@ -69,13 +69,17 @@ private fun LessonContentScope.meetHarry() { text("harry_hi") { text = textBuilder { line("Harry is a shape-shifting alien from the $HarryPlanet planet.") - line("Harry is a gifted scientist who can solve complex problems in a simple and elegant way.") - line("He's also quite impatient and tends to test his ideas at Friday on production.") + line("He is a gifted scientist who can solve complex problems in a simple and elegant way.") } } text("harry_hi2") { text = textBuilder { - line("He's also quite impatient and tends to test his ideas at Friday on production.") + line("Harry is also quite impatient and tends to test his ideas at Friday, directly on production.") + } + } + text("harry_hi3") { + text = textBuilder { + line("Why Harry!?") } } } @@ -91,7 +95,15 @@ private fun LessonContentScope.harryCrashed() { text("harry_crash") { text = textBuilder { line("Unfortunately, during one of his time-traveling experiments... ") + } + } + text("harry_crash2") { + text = textBuilder { line("Harry crashed his spaceship on Earth. (Oops!)") + } + } + text("harry_crash3") { + text = textBuilder { line("Now he needs your help to fix it and get back home.") line("But don't worry, Harry has a plan!") } From 493a8696064ebebbcbc088e9355cd3ef0d0a3657 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 23:16:47 +0300 Subject: [PATCH 07/10] WIP: Improve content --- .../ProgrammingMathInDisguise.kt | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt index dcd9ade..c153c92 100644 --- a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt +++ b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt @@ -174,13 +174,17 @@ private fun LessonContentScope.questionCoreTempKelvin() { private fun LessonContentScope.coreTempMathToHaskell() { text("core_temp_transition") { text = textBuilder { - line("Harry has successfully recalibrated the spaceship's energy core to $CoreTemp Kelvin.") + line("Harry has can now recalibrate the spaceship's energy core to $CoreTemp Kelvin.") line("But there's a catch! Harry's spaceship operates on an sophisticated Haskell-based system.") - line("Haskell is renowned for its ability to create type-safe and reliable programs, which is crucial for Harry's mission.") } style = TextStyle.BodySpacingLarge } text("core_temp_transition2") { + text = textBuilder { + line("Haskell is renowned for its ability to create type-safe and reliable programs, which is crucial for Harry's mission.") + } + } + text("core_temp_transition3") { text = textBuilder { line("Why Haskell? Here are a few reasons:") bullet("Type Safety: Helps prevent errors before they cause problems.") @@ -226,9 +230,9 @@ private fun LessonContentScope.questionCoreTempHaskell() { private fun LessonContentScope.contentKelvinToCelsiusMath() { text("core_temp_to_kelvin_conversion") { text = textBuilder { - line("Now that Harry has defined the core temperature function in Haskell, he needs to convert this temperature to Celsius.") - line("This will allow Harry to double-check the temperature with an Earth thermometer.") - line("Let's help Harry by defining the mathematical function to convert Kelvin to Celsius.") + line("Now that Harry has defined the core temperature function in Haskell.") + line("Next, he needs to convert this temperature to Celsius to double-check it with an Earth thermometer.") + line("Let's assist Harry by defining the mathematical function for converting Kelvin to Celsius.") } style = TextStyle.BodySpacingLarge } @@ -237,22 +241,22 @@ private fun LessonContentScope.contentKelvinToCelsiusMath() { private fun LessonContentScope.questionKelvinToCelsiusMath() { question("kelvinToCelsiusMath") { question = "What's the mathematical function to convert Kelvin to Celsius?" - clarification = "1°C = 274.15K (tip: do the math!)" + clarification = "1°C = 274.15K; 2°C = 275.15K; (tip: do the math!)" answer( text = "kelvinToCelsius(k) = k + 273.15", - explanation = "kelvinToCelsius(274.15) = 274.16 + 273.15 = 547.31°C. Incorrect.", + explanation = "Incorrect. 274.15 + 273.15 = 547.30°C, which is too high." ) answer( text = "kelvinToCelsius(k) = k - 274.15", - explanation = "kelvinToCelsius(274.15) = 274.15 - 274.15 = 0°C. Incorrect.", + explanation = "Incorrect. 274.15 - 274.15 = 0°C, which is not the correct conversion." ) answer( text = "kelvinToCelsius(k) = k * 274.15", - explanation = "kelvinToCelsius(10) = 10 * 274.15 = 2,741.5°C. Incorrect.", + explanation = "Incorrect. 10 * 274.15 = 2,741.5°C, which is far too high." ) answer( text = "kelvinToCelsius(k) = k - 273.15", - explanation = "That's correct! kelvinToCelsius(274.15) = 274.4 - 273.15 = 1°C", + explanation = "Correct! 274.15 - 273.15 = 1°C, the proper conversion.", correct = true, ) } @@ -261,7 +265,7 @@ private fun LessonContentScope.questionKelvinToCelsiusMath() { private fun LessonContentScope.contentKelvinToCelsiusHaskell() { text("kelvin_to_celsius_haskell") { text = textBuilder { - line("Harry needs to convert the Celsius conversion formula from math to Haskell to use it in his spaceship's system.") + line("Harry also needs to implement the formula in Haskell so he can use it in his spaceship's system.") } style = TextStyle.BodySpacingLarge } @@ -276,7 +280,7 @@ private fun LessonContentScope.questionKelvinToCelsiusHaskell() { line("kelvinToCelsius :: Double -> Double") line("kelvinToCelsius k = k - 273.15") }, - explanation = "The function accepts a Double parameter named \"k\" and returns Double", + explanation = "Correct. The function accepts a Double parameter named \"k\" and returns a Double.", correct = true, ) answer( @@ -284,24 +288,25 @@ private fun LessonContentScope.questionKelvinToCelsiusHaskell() { line("kelvinToCelsius :: Double") line("kelvinToCelsius k = k - 273.15") }, - explanation = "Compilation error. This function should accept a Double parameter.", + explanation = "Incorrect. Compilation error. This function should accept a Double parameter.", ) answer( text = codeBuilder { line("kelvinToCelsius :: Double -> Double") line("kelvinToCelsius = k - 273.15") }, - explanation = "Compilation error. Variable not in scope: k", + explanation = "Incorrect. Compilation error. Variable not in scope: k.", ) answer( text = codeBuilder { line("kelvinToCelsius k = k - 273.15") }, - explanation = "It compiles but let's specify types explicitly.", + explanation = "Incorrect. It compiles, but types should be specified explicitly.", ) } } + private fun LessonContentScope.contentKelvinToCelsiusToFahrenheitMath() { text("contentKelvinToCelsiusToFahrenheitMath") { text = textBuilder { From a42491c5ac93f7a7a5547dd46cc656d48f9ffdc8 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 23:21:45 +0300 Subject: [PATCH 08/10] Apply Nicole's feedback --- .../ProgrammingMathInDisguise.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt index c153c92..4263dc8 100644 --- a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt +++ b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt @@ -397,9 +397,6 @@ private fun LessonContentScope.functionComposition() { text = "Chapter 2: Function Composition" style = TextStyle.Heading } - image("functionComposition_img") { - imageUrl = "https://i.ibb.co/yqwLrq1/func-comp.jpg" - } text("functionComposition") { text = textBuilder { line("Great work! We now have all the parts to repair Harry's spaceship core.") @@ -407,12 +404,14 @@ private fun LessonContentScope.functionComposition() { } style = TextStyle.BodySpacingMedium } + image("functionComposition_img") { + imageUrl = "https://i.ibb.co/yqwLrq1/func-comp.jpg" + } text("functionComposition2") { text = textBuilder { line("In math, if we have a function f: A -> B and another function g: B -> C, we can combine them to form a new function h: A -> C.") line("This means we can apply f first, and then apply g to the result of f. Let's explore this concept further.") } - style = TextStyle.Body } } @@ -452,15 +451,19 @@ private fun LessonContentScope.functionCompositionOperator() { text = textBuilder { line("In mathematics, the function composition operator (.) is used to combine two or more functions.") line("This operator is read as 'after'. For example, the expression g . f means 'g after f'.") - line("When you see h(g(f(x))), it means you first apply f to x, then apply g to the result, and finally apply h to that result.") } style = TextStyle.BodySpacingLarge } text("function_composition_operator2") { text = textBuilder { - - line("Here's a tip: When reading function compositions, read the functions from right to left. This helps to understand the order in which the functions are applied.") - line("Now, let's see how this works in practice with the following question.") + line("When you see h(g(f(x))), it means you first apply f to x, then apply g to the result, and finally apply h to that result.") + line("This can also be written as (h . g . f)(x), which highlights the composition of multiple functions.") + } + } + text("function_composition_operator3") { + text = textBuilder { + line("Pro-tip: When reading function compositions, read the functions from right to left. This helps understand the order in which the functions are applied.") + line("Let's see how this works in practice with the next question.") } } } @@ -510,6 +513,7 @@ private fun LessonContentScope.finalSolution() { text = textBuilder { line("In the final question, we'll need to help Harry compose these functions together to power-up the core.") } + style = TextStyle.BodySpacingLarge } } From 356180ec432838698ca27d430bf49366eb0d71a4 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 23:31:38 +0300 Subject: [PATCH 09/10] Add joke AS image --- .../programmingfundamentals/ProgrammingMathInDisguise.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt index 4263dc8..c6f642b 100644 --- a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt +++ b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt @@ -315,11 +315,15 @@ private fun LessonContentScope.contentKelvinToCelsiusToFahrenheitMath() { } style = TextStyle.BodySpacingLarge } + image("android_studio_heater_img") { + imageUrl = "https://i.ibb.co/KKHNMPF/android-studio-heater.webp" + } text("contentKelvinToCelsiusToFahrenheitMath2") { text = textBuilder { line("He landed in a quirky town where the only available heater device is an old laptop running Android Studio, which only displays temperatures in Fahrenheit.") line("Let's help Harry convert the Celsius temperature to Fahrenheit mathematically so he can use the laptop to heat the core.") } + style = TextStyle.BodySpacingMedium } } From 5c1d7df57d897fc4ffb43e077917bb2eecb346f0 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Thu, 6 Jun 2024 23:45:48 +0300 Subject: [PATCH 10/10] More fixes --- .../ui/screen/lesson/composable/LessonAutoScroll.kt | 7 ++++--- .../ui/screen/lesson/composable/LessonContent.kt | 2 +- .../ProgrammingMathInDisguise.kt | 12 ++---------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt index 9507874..c520d7d 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonAutoScroll.kt @@ -10,19 +10,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import component.ScreenType.* import component.screenType -import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.delay import ui.screen.lesson.LessonItemViewState +import ui.screen.lesson.LessonViewState import ui.screen.lesson.OpenQuestionItemViewState import ui.screen.lesson.QuestionItemViewState @Composable fun AutoScrollEffect( listState: LazyListState, - items: ImmutableList, + viewState: LessonViewState, ) { + val items = viewState.items val itemsCount = items.size - LaunchedEffect(itemsCount) { + LaunchedEffect(itemsCount, viewState.cta) { if (itemsCount >= 2) { // ensure auto scrolls works for images that are loading repeat(4) { diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonContent.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonContent.kt index 3fd06df..0b9ec85 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonContent.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/composable/LessonContent.kt @@ -77,7 +77,7 @@ private fun LessonItemsLazyColum( AutoScrollEffect( listState = listState, - items = viewState.items, + viewState = viewState, ) LazyColumn( diff --git a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt index c6f642b..fe8493f 100644 --- a/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt +++ b/shared/src/commonMain/kotlin/ivy/content/lesson/programmingfundamentals/ProgrammingMathInDisguise.kt @@ -75,18 +75,14 @@ private fun LessonContentScope.meetHarry() { text("harry_hi2") { text = textBuilder { line("Harry is also quite impatient and tends to test his ideas at Friday, directly on production.") - } - } - text("harry_hi3") { - text = textBuilder { - line("Why Harry!?") + line("Why Harry!? Why?") } } } private fun LessonContentScope.harryCrashed() { text("harry_experimenting") { - text = "Harry loves experimenting... a lot!" + text = "Harry also loves experimenting... a lot!" style = TextStyle.Heading } lottie("harry_crash_anim") { @@ -100,10 +96,6 @@ private fun LessonContentScope.harryCrashed() { text("harry_crash2") { text = textBuilder { line("Harry crashed his spaceship on Earth. (Oops!)") - } - } - text("harry_crash3") { - text = textBuilder { line("Now he needs your help to fix it and get back home.") line("But don't worry, Harry has a plan!") }