diff --git a/composeApp/src/commonMain/kotlin/data/fake/FakeCourseRepository.kt b/composeApp/src/commonMain/kotlin/data/fake/FakeCourseRepository.kt index d348603e..9ada376f 100644 --- a/composeApp/src/commonMain/kotlin/data/fake/FakeCourseRepository.kt +++ b/composeApp/src/commonMain/kotlin/data/fake/FakeCourseRepository.kt @@ -6,13 +6,11 @@ import data.CourseRepository import ivy.content.EmptyContent import ivy.data.source.model.CourseResponse import ivy.model.* -import kotlinx.coroutines.delay class FakeCourseRepository : CourseRepository { override suspend fun fetchCourse( courseId: CourseId ): Either { - delay(1_000) return CourseResponse( course = Course( id = CourseId("1"), diff --git a/composeApp/src/commonMain/kotlin/data/fake/FakeLessonRepository.kt b/composeApp/src/commonMain/kotlin/data/fake/FakeLessonRepository.kt index e0db55da..dc37167f 100644 --- a/composeApp/src/commonMain/kotlin/data/fake/FakeLessonRepository.kt +++ b/composeApp/src/commonMain/kotlin/data/fake/FakeLessonRepository.kt @@ -42,4 +42,11 @@ class FakeLessonRepository : LessonRepository { ): Either { return Either.Right(Unit) } + + override suspend fun markLessonAsCompleted( + course: CourseId, + lesson: LessonId + ): Either { + return Either.Right(Unit) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/data/lesson/LessonRepository.kt b/composeApp/src/commonMain/kotlin/data/lesson/LessonRepository.kt index 17215e23..711f0c36 100644 --- a/composeApp/src/commonMain/kotlin/data/lesson/LessonRepository.kt +++ b/composeApp/src/commonMain/kotlin/data/lesson/LessonRepository.kt @@ -6,6 +6,7 @@ import data.lesson.mapper.LessonMapper import domain.SessionManager import domain.model.LessonProgress import domain.model.LessonWithProgress +import ivy.data.source.CoursesDataSource import ivy.data.source.LessonDataSource import ivy.model.CourseId import ivy.model.LessonId @@ -18,6 +19,7 @@ class LessonRepositoryImpl( private val datasource: LessonDataSource, private val mapper: LessonMapper, private val sessionManager: SessionManager, + private val coursesDataSource: CoursesDataSource, ) : LessonRepository { override suspend fun fetchLesson( @@ -56,7 +58,21 @@ class LessonRepositoryImpl( answered = progress.answered, completed = progress.completed, ) - ) + ).bind() + } + } + + override suspend fun markLessonAsCompleted( + course: CourseId, + lesson: LessonId, + ): Either = withContext(dispatchers.io) { + either { + val session = sessionManager.getSession().bind() + coursesDataSource.saveProgress( + session = session, + course = course, + lesson = lesson + ).bind() } } } @@ -72,4 +88,9 @@ interface LessonRepository { lesson: LessonId, progress: LessonProgress, ): Either + + suspend fun markLessonAsCompleted( + course: CourseId, + lesson: LessonId, + ): Either } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/EventHandler.kt b/composeApp/src/commonMain/kotlin/ui/EventHandler.kt index 546d208a..74a58448 100644 --- a/composeApp/src/commonMain/kotlin/ui/EventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/EventHandler.kt @@ -3,12 +3,12 @@ package ui import kotlinx.coroutines.CoroutineScope import kotlin.reflect.KClass -interface EventHandler { +interface EventHandler { val eventTypes: Set> - suspend fun VmContext.handleEvent(event: Event) + suspend fun VmContext.handleEvent(event: Event) } -interface VmContext { +interface VmContext { @EventHandlerDsl val state: State @@ -16,6 +16,8 @@ interface VmContext { fun modifyState(transformation: (State) -> State) val screenScope: CoroutineScope + + val args: Args } @DslMarker diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt index a6632263..48a4ab03 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt @@ -61,10 +61,15 @@ class LessonScreen( autoWire(::OnFinishClickEventHandler) autoWire(::OnChoiceClickEventHandler) register { - LessonViewModel( + LessonViewModel.Args( courseId = courseId, lessonId = lessonId, lessonName = lessonName, + ) + } + register { + LessonViewModel( + args = Di.get(), screenScope = screenScope, repository = Di.get(), viewStateMapper = Di.get(), diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt index 03ccf75e..3b731359 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonViewModel.kt @@ -16,18 +16,17 @@ import kotlinx.coroutines.launch import ui.ComposeViewModel import ui.EventHandler import ui.VmContext +import ui.screen.lesson.LessonViewModel.LocalState import ui.screen.lesson.mapper.LessonViewStateMapper import util.Logger class LessonViewModel( - private val courseId: CourseId, - private val lessonId: LessonId, - private val lessonName: String, + override val args: Args, override val screenScope: CoroutineScope, private val repository: LessonRepository, private val viewStateMapper: LessonViewStateMapper, - private val eventHandlers: Set>, + private val eventHandlers: Set>, private val analytics: Analytics, private val logger: Logger, ) : ComposeViewModel, LessonVmContext { @@ -46,8 +45,8 @@ class LessonViewModel( private fun saveLessonProgress(localState: LocalState) { screenScope.launch { repository.saveLessonProgress( - course = courseId, - lesson = lessonId, + course = args.courseId, + lesson = args.lessonId, progress = LessonProgress( selectedAnswers = localState.selectedAnswers, openAnswersInput = localState.openAnswersInput, @@ -69,9 +68,9 @@ class LessonViewModel( source = Source.Lesson, event = "view", params = mapOf( - Param.CourseId to courseId.value, - Param.LessonId to lessonId.value, - Param.LessonName to lessonName, + Param.CourseId to args.courseId.value, + Param.LessonId to args.lessonId.value, + Param.LessonName to args.lessonName, ) ) } @@ -83,7 +82,7 @@ class LessonViewModel( } null, is Either.Left -> LessonViewState( - title = lessonName, + title = args.lessonName, items = persistentListOf(), cta = null, progress = LessonProgressViewState(0, 1), @@ -94,8 +93,8 @@ class LessonViewModel( private suspend fun loadLesson() { lessonResponse = repository.fetchLesson( - course = courseId, - lesson = lessonId + course = args.courseId, + lesson = args.lessonId ).onRight { localState = LocalState( selectedAnswers = it.progress.selectedAnswers, @@ -115,11 +114,12 @@ class LessonViewModel( screenScope.launch { @Suppress("UNCHECKED_CAST") - val typedEventHandler = eventHandler as EventHandler + val typedEventHandler = eventHandler as LessonEventHandler with(typedEventHandler) { handleEvent(event) } } } + @Immutable @optics data class LocalState( val selectedAnswers: Map>, @@ -138,6 +138,13 @@ class LessonViewModel( ) } } + + data class Args( + val courseId: CourseId, + val lessonId: LessonId, + val lessonName: String, + ) } -typealias LessonVmContext = VmContext \ No newline at end of file +typealias LessonVmContext = VmContext +typealias LessonEventHandler = EventHandler \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnBackClickEventHandler.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnBackClickEventHandler.kt index 97ce99ce..3fc1e573 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnBackClickEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnBackClickEventHandler.kt @@ -1,14 +1,13 @@ package ui.screen.lesson.handler import navigation.Navigation -import ui.EventHandler +import ui.screen.lesson.LessonEventHandler import ui.screen.lesson.LessonViewEvent -import ui.screen.lesson.LessonViewModel import ui.screen.lesson.LessonVmContext class OnBackClickEventHandler( private val navigation: Navigation -) : EventHandler { +) : LessonEventHandler { override val eventTypes = setOf(LessonViewEvent.OnBackClick::class) override suspend fun LessonVmContext.handleEvent( 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 dc0e1d15..25703a65 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnChoiceClickEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnChoiceClickEventHandler.kt @@ -4,17 +4,13 @@ import arrow.optics.copy import domain.SoundUseCase import ivy.content.SoundsUrls import ivy.model.ChoiceOptionId -import ui.EventHandler -import ui.screen.lesson.LessonViewEvent +import ui.screen.lesson.* import ui.screen.lesson.LessonViewModel.LocalState -import ui.screen.lesson.LessonVmContext -import ui.screen.lesson.chosen -import ui.screen.lesson.completed import ui.screen.lesson.mapper.toDomain class OnChoiceClickEventHandler( private val soundUseCase: SoundUseCase -) : EventHandler { +) : LessonEventHandler { override val eventTypes = setOf(LessonViewEvent.OnChoiceClick::class) override suspend fun LessonVmContext.handleEvent(event: LessonViewEvent.OnChoiceClick) { diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnContinueClickEventHandler.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnContinueClickEventHandler.kt index 3458ebf7..842cd3be 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnContinueClickEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnContinueClickEventHandler.kt @@ -2,7 +2,7 @@ package ui.screen.lesson.handler import domain.SoundUseCase import ivy.content.SoundsUrls -import ui.EventHandler +import ui.screen.lesson.LessonEventHandler import ui.screen.lesson.LessonViewEvent import ui.screen.lesson.LessonViewModel.LocalState import ui.screen.lesson.LessonVmContext @@ -11,7 +11,7 @@ import ui.screen.lesson.mapper.toDomain class OnContinueClickEventHandler( private val soundUseCase: SoundUseCase -) : EventHandler { +) : LessonEventHandler { override val eventTypes = setOf(LessonViewEvent.OnContinueClick::class) override suspend fun LessonVmContext.handleEvent( diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnFinishClickEventHandler.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnFinishClickEventHandler.kt index f34a9657..ea66182d 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnFinishClickEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnFinishClickEventHandler.kt @@ -1,18 +1,20 @@ package ui.screen.lesson.handler +import data.lesson.LessonRepository import domain.SoundUseCase import ivy.content.SoundsUrls import navigation.Navigation -import ui.EventHandler +import ui.screen.lesson.LessonEventHandler import ui.screen.lesson.LessonViewEvent -import ui.screen.lesson.LessonViewModel.LocalState import ui.screen.lesson.LessonVmContext +import util.Logger class OnFinishClickEventHandler( private val navigation: Navigation, - private val soundUseCase: SoundUseCase -) : - EventHandler { + private val soundUseCase: SoundUseCase, + private val lessonRepository: LessonRepository, + private val logger: Logger, +) : LessonEventHandler { override val eventTypes = setOf(LessonViewEvent.OnFinishClick::class) override suspend fun LessonVmContext.handleEvent( @@ -20,5 +22,11 @@ class OnFinishClickEventHandler( ) { navigation.navigateBack() soundUseCase.playSound(SoundsUrls.CompleteLesson) + lessonRepository.markLessonAsCompleted( + course = args.courseId, + lesson = args.lessonId, + ).onLeft { errMsg -> + logger.error("Failed to mark lesson as completed because: $errMsg") + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnSoundClickEventHandler.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnSoundClickEventHandler.kt index 5887587e..8c55a49b 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnSoundClickEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/OnSoundClickEventHandler.kt @@ -1,14 +1,13 @@ package ui.screen.lesson.handler import domain.SoundUseCase -import ui.EventHandler +import ui.screen.lesson.LessonEventHandler import ui.screen.lesson.LessonViewEvent -import ui.screen.lesson.LessonViewModel import ui.screen.lesson.LessonVmContext class OnSoundClickEventHandler( private val soundUseCase: SoundUseCase -) : EventHandler { +) : LessonEventHandler { override val eventTypes = setOf(LessonViewEvent.OnSoundClick::class) override suspend fun LessonVmContext.handleEvent(event: LessonViewEvent.OnSoundClick) { diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionEventHandler.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionEventHandler.kt index e4c76fa2..4fe7dca5 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionEventHandler.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/handler/QuestionEventHandler.kt @@ -5,19 +5,15 @@ import arrow.optics.typeclasses.Index import domain.SoundUseCase import ivy.content.SoundsUrls import ivy.model.AnswerId -import ui.EventHandler +import ui.screen.lesson.* import ui.screen.lesson.LessonViewModel.LocalState -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.answered import ui.screen.lesson.mapper.toDomain -import ui.screen.lesson.selectedAnswers class QuestionEventHandler( private val soundUseCase: SoundUseCase -) : EventHandler { +) : LessonEventHandler { override val eventTypes = setOf( QuestionViewEvent.OnAnswerCheckChange::class, QuestionViewEvent.OnCheckClick::class, diff --git a/composeApp/src/jsMain/kotlin/navigation/SystemNavigation.js.kt b/composeApp/src/jsMain/kotlin/navigation/SystemNavigation.js.kt index 5f0d70d5..6cda5799 100644 --- a/composeApp/src/jsMain/kotlin/navigation/SystemNavigation.js.kt +++ b/composeApp/src/jsMain/kotlin/navigation/SystemNavigation.js.kt @@ -45,7 +45,7 @@ class WebSystemNavigation : SystemNavigation { } override fun navigateBack(): Boolean { - if (window.window.length <= 1) return false + if (window.history.length <= 1) return false window.history.back() // Let the browser handle back navigation // No need to call emitCurrentRoute() here; the listener will handle it