From 1bc1d4c97c496bce20780f5e2d3e2aaf0ab8fbd2 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk <141041606+PavloNetrebchuk@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:01:26 +0300 Subject: [PATCH] feat: [FC-0047] Course progress and collapsing sections (#323) * feat: Course Home progress bar * feat: Collapsing course sections * feat: New download icons * feat: show CourseContainerFragment if COURSE_NESTED_LIST_ENABLED false * fix: course progress bar updating * feat: Renamed COURSE_NESTED_LIST_ENABLE feature flag * feat: Course home. Moved certificate access. * chore: enhance app theme capability for prod edX theme/branding (#262) chore: enhance app theme capability for prod edX theme/branding - Integrate Program config updates - theming/branding code improvements for light and dark modes - Force dark mode for the WebView (beta version) - No major change in the Open edX theme fixes: LEARNER-9783 * feat: [FC-0047] Calendar main screen and dialogs (#322) * feat: Created calendar setting screen * feat: CalendarAccessDialog * feat: NewCalendarDialog * fix: Fixes according to PR feedback * fix: DiscussionTopicsViewModelTest.kt jUnit test * fix: assignment dates * feat: [FC-0047] Improved Dashboard Level Navigation (#308) * feat: Created Learn screen. Added course/program navigation. Added endpoint for UserCourses screen. * feat: Added primary course card * feat: Added start/resume course button * feat: Added alignment items * feat: Fix future assignment date, add courses list, add onSearch and onCourse clicks * feat: Add feature flag for enabling new/old dashboard screen, add UserCoursesScreen onClick methods * feat: Create AllEnrolledCoursesFragment. Add endpoint parameters * feat: AllEnrolledCoursesFragment UI * feat: Minor code refactoring, show cached data if no internet connection * feat: UserCourses screen data caching * feat: Dashboard * refactor: Dashboard type flag change, start course button change * feat: Added programs fragment to LearnFragment viewPager * feat: Empty states and settings button * fix: Number of courses * fix: Minor UI changes * fix: Fixes according to designer feedback * fix: Fixes after demo * refactor: Move CourseContainerTab * fix: Fixes according to PR feedback * fix: Fixes according to PR feedback * feat: added a patch from Omer Habib * fix: Fixes according to PR feedback * fix: Assignment date string * fix: Lint error * fix: Assignment date string * fix: Fixes according to PR feedback * fix: Fixes according to designer feedback * fix: Fixes according to PR feedback --------- Co-authored-by: Volodymyr Chekyrta Co-authored-by: Farhan Arshad <43750646+farhan-arshad-dev@users.noreply.github.com> --- Documentation/ConfigurationManagement.md | 2 +- .../java/org/openedx/core/config/UIConfig.kt | 4 +- .../core/data/model/AssignmentProgress.kt | 26 ++ .../java/org/openedx/core/data/model/Block.kt | 35 +- .../core/data/model/CourseStructureModel.kt | 11 +- .../openedx/core/data/model/room/BlockDb.kt | 70 ++-- .../data/model/room/CourseStructureEntity.kt | 8 +- .../core/domain/model/AssignmentProgress.kt | 7 + .../org/openedx/core/domain/model/Block.kt | 5 +- .../core/domain/model/CourseStructure.kt | 3 +- .../org/openedx/core/domain/model/Progress.kt | 9 + .../org/openedx/core/ui/theme/AppColors.kt | 7 +- .../java/org/openedx/core/ui/theme/Theme.kt | 14 +- .../java/org/openedx/core/utils/TimeUtils.kt | 40 +++ core/src/main/res/values/strings.xml | 19 ++ .../org/openedx/core/ui/theme/Colors.kt | 10 +- .../container/CourseContainerFragment.kt | 5 + .../container/CourseContainerViewModel.kt | 2 +- .../dates/CourseDatesViewModel.kt | 2 +- .../outline/CourseOutlineScreen.kt | 234 +++++++------- .../outline/CourseOutlineViewModel.kt | 21 +- .../section/CourseSectionFragment.kt | 8 +- .../course/presentation/ui/CourseUI.kt | 305 +++++++++++------- .../course/presentation/ui/CourseVideosUI.kt | 147 +++------ .../container/CourseUnitContainerViewModel.kt | 2 +- .../videos/CourseVideoViewModel.kt | 35 +- .../res/drawable/course_download_waiting.png | Bin 0 -> 1945 bytes .../res/drawable/course_ic_start_download.xml | 28 +- course/src/main/res/values/strings.xml | 10 + .../container/CourseContainerViewModelTest.kt | 20 +- .../dates/CourseDatesViewModelTest.kt | 1 + .../outline/CourseOutlineViewModelTest.kt | 34 +- .../section/CourseSectionViewModelTest.kt | 21 +- .../CourseUnitContainerViewModelTest.kt | 26 +- .../videos/CourseVideoViewModelTest.kt | 36 ++- .../presentation/DashboardGalleryView.kt | 13 +- dashboard/src/main/res/values/strings.xml | 12 +- default_config/dev/config.yaml | 1 - .../topics/DiscussionTopicsViewModelTest.kt | 22 +- .../presentation/settings/SettingsScreenUI.kt | 2 +- 40 files changed, 770 insertions(+), 487 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/data/model/AssignmentProgress.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/AssignmentProgress.kt create mode 100644 course/src/main/res/drawable/course_download_waiting.png diff --git a/Documentation/ConfigurationManagement.md b/Documentation/ConfigurationManagement.md index b1e21a50b..c3786b1d6 100644 --- a/Documentation/ConfigurationManagement.md +++ b/Documentation/ConfigurationManagement.md @@ -88,7 +88,7 @@ android: - **PRE_LOGIN_EXPERIENCE_ENABLED:** Enables the pre login courses discovery experience. - **WHATS_NEW_ENABLED:** Enables the "What's New" feature to present the latest changes to the user. - **SOCIAL_AUTH_ENABLED:** Enables SSO buttons on the SignIn and SignUp screens. -- **COURSE_NESTED_LIST_ENABLED:** Enables an alternative visual representation for the course structure. +- **COURSE_DROPDOWN_NAVIGATION_ENABLED:** Enables an alternative navigation through units. - **COURSE_UNIT_PROGRESS_ENABLED:** Enables the display of the unit progress within the courseware. ## Future Support diff --git a/core/src/main/java/org/openedx/core/config/UIConfig.kt b/core/src/main/java/org/openedx/core/config/UIConfig.kt index 1f8443d27..86c5d6b2b 100644 --- a/core/src/main/java/org/openedx/core/config/UIConfig.kt +++ b/core/src/main/java/org/openedx/core/config/UIConfig.kt @@ -3,8 +3,8 @@ package org.openedx.core.config import com.google.gson.annotations.SerializedName data class UIConfig( - @SerializedName("COURSE_NESTED_LIST_ENABLED") - val isCourseNestedListEnabled: Boolean = false, + @SerializedName("COURSE_DROPDOWN_NAVIGATION_ENABLED") + val isCourseDropdownNavigationEnabled: Boolean = false, @SerializedName("COURSE_UNIT_PROGRESS_ENABLED") val isCourseUnitProgressEnabled: Boolean = false, ) diff --git a/core/src/main/java/org/openedx/core/data/model/AssignmentProgress.kt b/core/src/main/java/org/openedx/core/data/model/AssignmentProgress.kt new file mode 100644 index 000000000..2ac10cb18 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/AssignmentProgress.kt @@ -0,0 +1,26 @@ +package org.openedx.core.data.model + +import com.google.gson.annotations.SerializedName +import org.openedx.core.data.model.room.AssignmentProgressDb +import org.openedx.core.domain.model.AssignmentProgress + +data class AssignmentProgress( + @SerializedName("assignment_type") + val assignmentType: String?, + @SerializedName("num_points_earned") + val numPointsEarned: Float?, + @SerializedName("num_points_possible") + val numPointsPossible: Float?, +) { + fun mapToDomain() = AssignmentProgress( + assignmentType = assignmentType ?: "", + numPointsEarned = numPointsEarned ?: 0f, + numPointsPossible = numPointsPossible ?: 0f + ) + + fun mapToRoomEntity() = AssignmentProgressDb( + assignmentType = assignmentType, + numPointsEarned = numPointsEarned, + numPointsPossible = numPointsPossible + ) +} diff --git a/core/src/main/java/org/openedx/core/data/model/Block.kt b/core/src/main/java/org/openedx/core/data/model/Block.kt index 9c07367ac..b5581209f 100644 --- a/core/src/main/java/org/openedx/core/data/model/Block.kt +++ b/core/src/main/java/org/openedx/core/data/model/Block.kt @@ -2,7 +2,12 @@ package org.openedx.core.data.model import com.google.gson.annotations.SerializedName import org.openedx.core.BlockType -import org.openedx.core.domain.model.Block +import org.openedx.core.utils.TimeUtils +import org.openedx.core.domain.model.Block as DomainBlock +import org.openedx.core.domain.model.BlockCounts as DomainBlockCounts +import org.openedx.core.domain.model.EncodedVideos as DomainEncodedVideos +import org.openedx.core.domain.model.StudentViewData as DomainStudentViewData +import org.openedx.core.domain.model.VideoInfo as DomainVideoInfo data class Block( @SerializedName("id") @@ -33,8 +38,12 @@ data class Block( val completion: Double?, @SerializedName("contains_gated_content") val containsGatedContent: Boolean?, + @SerializedName("assignment_progress") + val assignmentProgress: AssignmentProgress?, + @SerializedName("due") + val due: String? ) { - fun mapToDomain(blockData: Map): Block { + fun mapToDomain(blockData: Map): DomainBlock { val blockType = BlockType.getBlockType(type ?: "") val descendantsType = if (blockType == BlockType.VERTICAL) { val types = descendants?.map { descendant -> @@ -46,7 +55,7 @@ data class Block( blockType } - return org.openedx.core.domain.model.Block( + return DomainBlock( id = id ?: "", blockId = blockId ?: "", lmsWebUrl = lmsWebUrl ?: "", @@ -61,7 +70,9 @@ data class Block( studentViewMultiDevice = studentViewMultiDevice ?: false, blockCounts = blockCounts?.mapToDomain()!!, completion = completion ?: 0.0, - containsGatedContent = containsGatedContent ?: false + containsGatedContent = containsGatedContent ?: false, + assignmentProgress = assignmentProgress?.mapToDomain(), + due = TimeUtils.iso8601ToDate(due ?: ""), ) } } @@ -80,8 +91,8 @@ data class StudentViewData( @SerializedName("topic_id") val topicId: String? ) { - fun mapToDomain(): org.openedx.core.domain.model.StudentViewData { - return org.openedx.core.domain.model.StudentViewData( + fun mapToDomain(): DomainStudentViewData { + return DomainStudentViewData( onlyOnWeb = onlyOnWeb ?: false, duration = duration ?: "", transcripts = transcripts, @@ -106,8 +117,8 @@ data class EncodedVideos( var mobileLow: VideoInfo? ) { - fun mapToDomain(): org.openedx.core.domain.model.EncodedVideos { - return org.openedx.core.domain.model.EncodedVideos( + fun mapToDomain(): DomainEncodedVideos { + return DomainEncodedVideos( youtube = videoInfo?.mapToDomain(), hls = hls?.mapToDomain(), fallback = fallback?.mapToDomain(), @@ -124,8 +135,8 @@ data class VideoInfo( @SerializedName("file_size") var fileSize: Int? ) { - fun mapToDomain(): org.openedx.core.domain.model.VideoInfo { - return org.openedx.core.domain.model.VideoInfo( + fun mapToDomain(): DomainVideoInfo { + return DomainVideoInfo( url = url ?: "", fileSize = fileSize ?: 0 ) @@ -136,8 +147,8 @@ data class BlockCounts( @SerializedName("video") var video: Int? ) { - fun mapToDomain(): org.openedx.core.domain.model.BlockCounts { - return org.openedx.core.domain.model.BlockCounts( + fun mapToDomain(): DomainBlockCounts { + return DomainBlockCounts( video = video ?: 0 ) } diff --git a/core/src/main/java/org/openedx/core/data/model/CourseStructureModel.kt b/core/src/main/java/org/openedx/core/data/model/CourseStructureModel.kt index 9f22a14a0..d09411d14 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseStructureModel.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseStructureModel.kt @@ -4,6 +4,7 @@ import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.room.BlockDb import org.openedx.core.data.model.room.CourseStructureEntity import org.openedx.core.data.model.room.MediaDb +import org.openedx.core.data.model.room.discovery.ProgressDb import org.openedx.core.domain.model.CourseStructure import org.openedx.core.utils.TimeUtils @@ -35,7 +36,9 @@ data class CourseStructureModel( @SerializedName("certificate") val certificate: Certificate?, @SerializedName("is_self_paced") - var isSelfPaced: Boolean? + var isSelfPaced: Boolean?, + @SerializedName("course_progress") + val progress: Progress?, ) { fun mapToDomain(): CourseStructure { return CourseStructure( @@ -54,7 +57,8 @@ data class CourseStructureModel( coursewareAccess = coursewareAccess?.mapToDomain(), media = media?.mapToDomain(), certificate = certificate?.mapToDomain(), - isSelfPaced = isSelfPaced ?: false + isSelfPaced = isSelfPaced ?: false, + progress = progress?.mapToDomain() ) } @@ -73,7 +77,8 @@ data class CourseStructureModel( coursewareAccess = coursewareAccess?.mapToRoomEntity(), media = MediaDb.createFrom(media), certificate = certificate?.mapToRoomEntity(), - isSelfPaced = isSelfPaced ?: false + isSelfPaced = isSelfPaced ?: false, + progress = progress?.mapToRoomEntity() ?: ProgressDb.DEFAULT_PROGRESS ) } } diff --git a/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt b/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt index b1e9a53cf..737437dd0 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt @@ -3,7 +3,18 @@ package org.openedx.core.data.model.room import androidx.room.ColumnInfo import androidx.room.Embedded import org.openedx.core.BlockType -import org.openedx.core.domain.model.* +import org.openedx.core.data.model.Block +import org.openedx.core.data.model.BlockCounts +import org.openedx.core.data.model.EncodedVideos +import org.openedx.core.data.model.StudentViewData +import org.openedx.core.data.model.VideoInfo +import org.openedx.core.utils.TimeUtils +import org.openedx.core.domain.model.AssignmentProgress as DomainAssignmentProgress +import org.openedx.core.domain.model.Block as DomainBlock +import org.openedx.core.domain.model.BlockCounts as DomainBlockCounts +import org.openedx.core.domain.model.EncodedVideos as DomainEncodedVideos +import org.openedx.core.domain.model.StudentViewData as DomainStudentViewData +import org.openedx.core.domain.model.VideoInfo as DomainVideoInfo data class BlockDb( @ColumnInfo("id") @@ -33,9 +44,13 @@ data class BlockDb( @ColumnInfo("completion") val completion: Double, @ColumnInfo("contains_gated_content") - val containsGatedContent: Boolean + val containsGatedContent: Boolean, + @Embedded + val assignmentProgress: AssignmentProgressDb?, + @ColumnInfo("due") + val due: String? ) { - fun mapToDomain(blocks: List): Block { + fun mapToDomain(blocks: List): DomainBlock { val blockType = BlockType.getBlockType(type) val descendantsType = if (blockType == BlockType.VERTICAL) { val types = descendants.map { descendant -> @@ -47,7 +62,7 @@ data class BlockDb( blockType } - return Block( + return DomainBlock( id = id, blockId = blockId, lmsWebUrl = lmsWebUrl, @@ -62,14 +77,16 @@ data class BlockDb( descendants = descendants, descendantsType = descendantsType, completion = completion, - containsGatedContent = containsGatedContent + containsGatedContent = containsGatedContent, + assignmentProgress = assignmentProgress?.mapToDomain(), + due = TimeUtils.iso8601ToDate(due ?: ""), ) } companion object { fun createFrom( - block: org.openedx.core.data.model.Block + block: Block ): BlockDb { with(block) { return BlockDb( @@ -86,7 +103,9 @@ data class BlockDb( studentViewMultiDevice = studentViewMultiDevice ?: false, blockCounts = BlockCountsDb.createFrom(blockCounts), completion = completion ?: 0.0, - containsGatedContent = containsGatedContent ?: false + containsGatedContent = containsGatedContent ?: false, + assignmentProgress = assignmentProgress?.mapToRoomEntity(), + due = due ) } } @@ -105,8 +124,8 @@ data class StudentViewDataDb( @Embedded val encodedVideos: EncodedVideosDb? ) { - fun mapToDomain(): StudentViewData { - return StudentViewData( + fun mapToDomain(): DomainStudentViewData { + return DomainStudentViewData( onlyOnWeb, duration, transcripts, @@ -117,7 +136,7 @@ data class StudentViewDataDb( companion object { - fun createFrom(studentViewData: org.openedx.core.data.model.StudentViewData?): StudentViewDataDb { + fun createFrom(studentViewData: StudentViewData?): StudentViewDataDb { return StudentViewDataDb( onlyOnWeb = studentViewData?.onlyOnWeb ?: false, duration = studentViewData?.duration.toString(), @@ -144,9 +163,9 @@ data class EncodedVideosDb( @ColumnInfo("mobileLow") var mobileLow: VideoInfoDb? ) { - fun mapToDomain(): EncodedVideos { - return EncodedVideos( - youtube?.mapToDomain(), + fun mapToDomain(): DomainEncodedVideos { + return DomainEncodedVideos( + youtube = youtube?.mapToDomain(), hls = hls?.mapToDomain(), fallback = fallback?.mapToDomain(), desktopMp4 = desktopMp4?.mapToDomain(), @@ -156,7 +175,7 @@ data class EncodedVideosDb( } companion object { - fun createFrom(encodedVideos: org.openedx.core.data.model.EncodedVideos?): EncodedVideosDb { + fun createFrom(encodedVideos: EncodedVideos?): EncodedVideosDb { return EncodedVideosDb( youtube = VideoInfoDb.createFrom(encodedVideos?.videoInfo), hls = VideoInfoDb.createFrom(encodedVideos?.hls), @@ -176,10 +195,10 @@ data class VideoInfoDb( @ColumnInfo("fileSize") val fileSize: Int ) { - fun mapToDomain() = VideoInfo(url, fileSize) + fun mapToDomain() = DomainVideoInfo(url, fileSize) companion object { - fun createFrom(videoInfo: org.openedx.core.data.model.VideoInfo?): VideoInfoDb? { + fun createFrom(videoInfo: VideoInfo?): VideoInfoDb? { if (videoInfo == null) return null return VideoInfoDb( videoInfo.url ?: "", @@ -193,11 +212,26 @@ data class BlockCountsDb( @ColumnInfo("video") val video: Int ) { - fun mapToDomain() = BlockCounts(video) + fun mapToDomain() = DomainBlockCounts(video) companion object { - fun createFrom(blocksCounts: org.openedx.core.data.model.BlockCounts?): BlockCountsDb { + fun createFrom(blocksCounts: BlockCounts?): BlockCountsDb { return BlockCountsDb(blocksCounts?.video ?: 0) } } } + +data class AssignmentProgressDb( + @ColumnInfo("assignment_type") + val assignmentType: String?, + @ColumnInfo("num_points_earned") + val numPointsEarned: Float?, + @ColumnInfo("num_points_possible") + val numPointsPossible: Float?, +) { + fun mapToDomain() = DomainAssignmentProgress( + assignmentType = assignmentType ?: "", + numPointsEarned = numPointsEarned ?: 0f, + numPointsPossible = numPointsPossible ?: 0f + ) +} diff --git a/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt index 90352d821..49862d683 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt @@ -6,6 +6,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey import org.openedx.core.data.model.room.discovery.CertificateDb import org.openedx.core.data.model.room.discovery.CoursewareAccessDb +import org.openedx.core.data.model.room.discovery.ProgressDb import org.openedx.core.domain.model.CourseStructure import org.openedx.core.utils.TimeUtils @@ -39,7 +40,9 @@ data class CourseStructureEntity( @Embedded val certificate: CertificateDb?, @ColumnInfo("isSelfPaced") - val isSelfPaced: Boolean + val isSelfPaced: Boolean, + @Embedded + val progress: ProgressDb, ) { fun mapToDomain(): CourseStructure { @@ -57,7 +60,8 @@ data class CourseStructureEntity( coursewareAccess?.mapToDomain(), media?.mapToDomain(), certificate?.mapToDomain(), - isSelfPaced + isSelfPaced, + progress.mapToDomain() ) } diff --git a/core/src/main/java/org/openedx/core/domain/model/AssignmentProgress.kt b/core/src/main/java/org/openedx/core/domain/model/AssignmentProgress.kt new file mode 100644 index 000000000..659665bfe --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/AssignmentProgress.kt @@ -0,0 +1,7 @@ +package org.openedx.core.domain.model + +data class AssignmentProgress( + val assignmentType: String, + val numPointsEarned: Float, + val numPointsPossible: Float +) diff --git a/core/src/main/java/org/openedx/core/domain/model/Block.kt b/core/src/main/java/org/openedx/core/domain/model/Block.kt index 2f1766ecb..460f283ba 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Block.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Block.kt @@ -7,6 +7,7 @@ import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadedState import org.openedx.core.module.db.FileType import org.openedx.core.utils.VideoUtil +import java.util.Date data class Block( @@ -25,7 +26,9 @@ data class Block( val descendantsType: BlockType, val completion: Double, val containsGatedContent: Boolean = false, - val downloadModel: DownloadModel? = null + val downloadModel: DownloadModel? = null, + val assignmentProgress: AssignmentProgress?, + val due: Date? ) { val isDownloadable: Boolean get() { diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt b/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt index bdb3820de..4ba3a8419 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseStructure.kt @@ -16,5 +16,6 @@ data class CourseStructure( val coursewareAccess: CoursewareAccess?, val media: Media?, val certificate: Certificate?, - val isSelfPaced: Boolean + val isSelfPaced: Boolean, + val progress: Progress?, ) diff --git a/core/src/main/java/org/openedx/core/domain/model/Progress.kt b/core/src/main/java/org/openedx/core/domain/model/Progress.kt index 5d8ea19f8..800a9c292 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Progress.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Progress.kt @@ -1,6 +1,7 @@ package org.openedx.core.domain.model import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize @@ -8,6 +9,14 @@ data class Progress( val assignmentsCompleted: Int, val totalAssignmentsCount: Int, ) : Parcelable { + + @IgnoredOnParcel + val value: Float = try { + assignmentsCompleted.toFloat() / totalAssignmentsCount.toFloat() + } catch (_: ArithmeticException) { + 0f + } + companion object { val DEFAULT_PROGRESS = Progress(0, 0) } diff --git a/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt b/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt index 968fd9fe3..625c52b27 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt @@ -50,7 +50,7 @@ data class AppColors( val inactiveButtonBackground: Color, val inactiveButtonText: Color, - val accessGreen: Color, + val successGreen: Color, val datesSectionBarPastDue: Color, val datesSectionBarToday: Color, @@ -73,7 +73,10 @@ data class AppColors( val courseHomeHeaderShade: Color, val courseHomeBackBtnBackground: Color, - val settingsTitleContent: Color + val settingsTitleContent: Color, + + val progressBarColor: Color, + val progressBarBackgroundColor: Color ) { val primary: Color get() = material.primary val primaryVariant: Color get() = material.primaryVariant diff --git a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt index 291192c1c..88c973105 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt @@ -68,7 +68,7 @@ private val DarkColorPalette = AppColors( inactiveButtonBackground = dark_inactive_button_background, inactiveButtonText = dark_primary_button_text, - accessGreen = dark_access_green, + successGreen = dark_success_green, datesSectionBarPastDue = dark_dates_section_bar_past_due, datesSectionBarToday = dark_dates_section_bar_today, @@ -91,7 +91,10 @@ private val DarkColorPalette = AppColors( courseHomeHeaderShade = dark_course_home_header_shade, courseHomeBackBtnBackground = dark_course_home_back_btn_background, - settingsTitleContent = dark_settings_title_content + settingsTitleContent = dark_settings_title_content, + + progressBarColor = dark_progress_bar_color, + progressBarBackgroundColor = dark_progress_bar_background_color ) private val LightColorPalette = AppColors( @@ -152,7 +155,7 @@ private val LightColorPalette = AppColors( inactiveButtonBackground = light_inactive_button_background, inactiveButtonText = light_primary_button_text, - accessGreen = light_access_green, + successGreen = light_success_green, datesSectionBarPastDue = light_dates_section_bar_past_due, datesSectionBarToday = light_dates_section_bar_today, @@ -175,7 +178,10 @@ private val LightColorPalette = AppColors( courseHomeHeaderShade = light_course_home_header_shade, courseHomeBackBtnBackground = light_course_home_back_btn_background, - settingsTitleContent = light_settings_title_content + settingsTitleContent = light_settings_title_content, + + progressBarColor = light_progress_bar_color, + progressBarBackgroundColor = light_progress_bar_background_color ) val MaterialTheme.appColors: AppColors diff --git a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt index 9ccfaebef..5327b8cf5 100644 --- a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt @@ -242,6 +242,46 @@ object TimeUtils { } } + fun getAssignmentFormattedDate(context: Context, date: Date): String { + val inputDate = Calendar.getInstance().also { + it.time = date + it.clearTimeComponents() + } + val daysDifference = getDayDifference(inputDate) + + return when { + daysDifference == 0 -> { + context.getString(R.string.core_date_format_assignment_due_today) + } + + daysDifference == 1 -> { + context.getString(R.string.core_date_format_assignment_due_tomorrow) + } + + daysDifference == -1 -> { + context.getString(R.string.core_date_format_assignment_due_yesterday) + } + + daysDifference <= -2 -> { + val numberOfDays = ceil(-daysDifference.toDouble()).toInt() + context.resources.getQuantityString( + R.plurals.core_date_format_assignment_due_days_ago, + numberOfDays, + numberOfDays + ) + } + + else -> { + val numberOfDays = ceil(daysDifference.toDouble()).toInt() + context.resources.getQuantityString( + R.plurals.core_date_format_assignment_due_in, + numberOfDays, + numberOfDays + ) + } + } + } + /** * Returns the number of days difference between the given date and the current date. */ diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index afbc28243..580d262ac 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -96,6 +96,25 @@ Tomorrow Yesterday %1$s days ago + Due Today + Due Tomorrow + Due Yesterday + + Due %1$d days ago + Due %1$d day ago + Due %1$d days ago + Due %1$d days ago + Due %1$d days ago + Due %1$d days ago + + + Due in %1$d days + Due in %1$d day + Due in %1$d days + Due in %1$d days + Due in %1$d days + Due in %1$d days + %d Item Hidden %d Items Hidden diff --git a/core/src/openedx/org/openedx/core/ui/theme/Colors.kt b/core/src/openedx/org/openedx/core/ui/theme/Colors.kt index 0bb1114c8..855d557d3 100644 --- a/core/src/openedx/org/openedx/core/ui/theme/Colors.kt +++ b/core/src/openedx/org/openedx/core/ui/theme/Colors.kt @@ -51,7 +51,7 @@ val light_warning = Color(0xFFFFC94D) val light_info = Color(0xFF42AAFF) val light_rate_stars = Color(0xFFFFC94D) val light_inactive_button_background = Color(0xFFCCD4E0) -val light_access_green = Color(0xFF23BCA0) +val light_success_green = Color(0xFF198571) val light_dates_section_bar_past_due = light_warning val light_dates_section_bar_today = light_info val light_dates_section_bar_this_week = light_text_primary_variant @@ -70,9 +70,11 @@ val light_tab_selected_btn_content = Color.White val light_course_home_header_shade = Color(0xFFBABABA) val light_course_home_back_btn_background = Color.White val light_settings_title_content = Color.White +val light_progress_bar_color = light_primary +val light_progress_bar_background_color = Color(0xFF97A5BB) -val dark_primary = Color(0xFF5478F9) +val dark_primary = Color(0xFF3F68F8) val dark_primary_variant = Color(0xFF3700B3) val dark_secondary = Color(0xFF03DAC6) val dark_secondary_variant = Color(0xFF373E4F) @@ -121,7 +123,7 @@ val dark_info_variant = Color(0xFF5478F9) val dark_onInfo = Color.White val dark_rate_stars = Color(0xFFFFC94D) val dark_inactive_button_background = Color(0xFFCCD4E0) -val dark_access_green = Color(0xFF23BCA0) +val dark_success_green = Color(0xFF198571) val dark_dates_section_bar_past_due = dark_warning val dark_dates_section_bar_today = dark_info val dark_dates_section_bar_this_week = dark_text_primary_variant @@ -140,3 +142,5 @@ val dark_tab_selected_btn_content = Color.White val dark_course_home_header_shade = Color(0xFF999999) val dark_course_home_back_btn_background = Color.Black val dark_settings_title_content = Color.White +val dark_progress_bar_color = light_primary +val dark_progress_bar_background_color = Color(0xFF8E9BAE) diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index a55ca6bc9..4df1fcf64 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -113,6 +113,11 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { observe() } + override fun onResume() { + super.onResume() + viewModel.updateData() + } + override fun onDestroyView() { snackBar?.dismiss() super.onDestroyView() diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index bbc26d535..4e233e3d7 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -169,7 +169,7 @@ class CourseContainerViewModel( _showProgress.value = true viewModelScope.launch { try { - val courseStructure = interactor.getCourseStructure(courseId) + val courseStructure = interactor.getCourseStructure(courseId, true) courseName = courseStructure.name _organization = courseStructure.org _isSelfPaced = courseStructure.isSelfPaced diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index a6a78cb72..4d6236b67 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -77,7 +77,7 @@ class CourseDatesViewModel( private var courseBannerType: CourseBannerType = CourseBannerType.BLANK private var courseStructure: CourseStructure? = null - val isCourseExpandableSectionsEnabled get() = config.getCourseUIConfig().isCourseNestedListEnabled + val isCourseExpandableSectionsEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled init { viewModelScope.launch { diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt index 9903578dc..1a29819f2 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt @@ -2,7 +2,6 @@ package org.openedx.course.presentation.outline import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,9 +16,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Divider +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface @@ -33,9 +32,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.AndroidUriHandler import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices @@ -45,11 +46,13 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager import org.openedx.core.BlockType import org.openedx.core.UIMessage +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseDatesBannerInfo import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.domain.model.Progress import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.ui.HandleUIMessage @@ -66,10 +69,8 @@ import org.openedx.core.utils.FileUtil import org.openedx.course.R import org.openedx.course.presentation.ui.CourseDatesBanner import org.openedx.course.presentation.ui.CourseDatesBannerTablet -import org.openedx.course.presentation.ui.CourseExpandableChapterCard import org.openedx.course.presentation.ui.CourseMessage -import org.openedx.course.presentation.ui.CourseSectionCard -import org.openedx.course.presentation.ui.CourseSubSectionItem +import org.openedx.course.presentation.ui.CourseSection import java.util.Date import org.openedx.core.R as CoreR @@ -94,20 +95,7 @@ fun CourseOutlineScreen( CourseOutlineUI( windowSize = windowSize, uiState = uiState, - isCourseNestedListEnabled = viewModel.isCourseNestedListEnabled, uiMessage = uiMessage, - onItemClick = { block -> - viewModel.sequentialClickedEvent( - block.blockId, - block.displayName - ) - viewModel.courseRouter.navigateToCourseSubsections( - fm = fragmentManager, - courseId = viewModel.courseId, - subSectionId = block.id, - mode = CourseViewMode.FULL - ) - }, onExpandClick = { block -> if (viewModel.switchCourseSections(block.id)) { viewModel.sequentialClickedEvent( @@ -117,15 +105,28 @@ fun CourseOutlineScreen( } }, onSubSectionClick = { subSectionBlock -> - viewModel.courseSubSectionUnit[subSectionBlock.id]?.let { unit -> - viewModel.logUnitDetailViewedEvent( - unit.blockId, - unit.displayName + if (viewModel.isCourseNestedListEnabled) { + viewModel.courseSubSectionUnit[subSectionBlock.id]?.let { unit -> + viewModel.logUnitDetailViewedEvent( + unit.blockId, + unit.displayName + ) + viewModel.courseRouter.navigateToCourseContainer( + fragmentManager, + courseId = viewModel.courseId, + unitId = unit.id, + mode = CourseViewMode.FULL + ) + } + } else { + viewModel.sequentialClickedEvent( + subSectionBlock.blockId, + subSectionBlock.displayName ) - viewModel.courseRouter.navigateToCourseContainer( - fragmentManager, + viewModel.courseRouter.navigateToCourseSubsections( + fm = fragmentManager, courseId = viewModel.courseId, - unitId = unit.id, + subSectionId = subSectionBlock.id, mode = CourseViewMode.FULL ) } @@ -136,19 +137,21 @@ fun CourseOutlineScreen( componentId ) }, - onDownloadClick = { - if (viewModel.isBlockDownloading(it.id)) { - viewModel.courseRouter.navigateToDownloadQueue( - fm = fragmentManager, - viewModel.getDownloadableChildren(it.id) - ?: arrayListOf() - ) - } else if (viewModel.isBlockDownloaded(it.id)) { - viewModel.removeDownloadModels(it.id) - } else { - viewModel.saveDownloadModels( - FileUtil(context).getExternalAppDir().path, it.id - ) + onDownloadClick = { blocksIds -> + blocksIds.forEach { blockId -> + if (viewModel.isBlockDownloading(blockId)) { + viewModel.courseRouter.navigateToDownloadQueue( + fm = fragmentManager, + viewModel.getDownloadableChildren(blockId) + ?: arrayListOf() + ) + } else if (viewModel.isBlockDownloaded(blockId)) { + viewModel.removeDownloadModels(blockId) + } else { + viewModel.saveDownloadModels( + FileUtil(context).getExternalAppDir().path, blockId + ) + } } }, onResetDatesClick = { @@ -170,13 +173,11 @@ fun CourseOutlineScreen( private fun CourseOutlineUI( windowSize: WindowSize, uiState: CourseOutlineUIState, - isCourseNestedListEnabled: Boolean, uiMessage: UIMessage?, - onItemClick: (Block) -> Unit, onExpandClick: (Block) -> Unit, onSubSectionClick: (Block) -> Unit, onResumeClick: (String) -> Unit, - onDownloadClick: (Block) -> Unit, + onDownloadClick: (blockIds: List) -> Unit, onResetDatesClick: () -> Unit, onCertificateClick: (String) -> Unit, ) { @@ -278,6 +279,19 @@ private fun CourseOutlineUI( } } + + val progress = uiState.courseStructure.progress + if (progress != null && progress.totalAssignmentsCount > 0) { + item { + CourseProgress( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 24.dp, end = 24.dp), + progress = progress + ) + } + } + if (uiState.resumeComponent != null) { item { Box(listPadding) { @@ -298,75 +312,26 @@ private fun CourseOutlineUI( } } - if (isCourseNestedListEnabled) { - uiState.courseStructure.blockData.forEach { section -> - val courseSubSections = - uiState.courseSubSections[section.id] - val courseSectionsState = - uiState.courseSectionsState[section.id] - - item { - Column { - CourseExpandableChapterCard( - modifier = listPadding, - block = section, - onItemClick = onExpandClick, - arrowDegrees = if (courseSectionsState == true) -90f else 90f - ) - Divider() - } - } - - courseSubSections?.forEach { subSectionBlock -> - item { - Column { - AnimatedVisibility( - visible = courseSectionsState == true - ) { - Column { - val downloadsCount = - uiState.subSectionsDownloadsCount[subSectionBlock.id] - ?: 0 - - CourseSubSectionItem( - modifier = listPadding, - block = subSectionBlock, - downloadedState = uiState.downloadedState[subSectionBlock.id], - downloadsCount = downloadsCount, - onClick = onSubSectionClick, - onDownloadClick = onDownloadClick - ) - Divider() - } - } - } - } - } - } - return@LazyColumn + item { + Spacer(modifier = Modifier.height(12.dp)) } + uiState.courseStructure.blockData.forEach { section -> + val courseSubSections = + uiState.courseSubSections[section.id] + val courseSectionsState = + uiState.courseSectionsState[section.id] - items(uiState.courseStructure.blockData) { block -> - Column(listPadding) { - if (block.type == BlockType.CHAPTER) { - Text( - modifier = Modifier.padding( - top = 36.dp, - bottom = 8.dp - ), - text = block.displayName, - style = MaterialTheme.appTypography.titleMedium, - color = MaterialTheme.appColors.textPrimaryVariant - ) - } else { - CourseSectionCard( - block = block, - downloadedState = uiState.downloadedState[block.id], - onItemClick = onItemClick, - onDownloadClick = onDownloadClick - ) - Divider() - } + item { + CourseSection( + modifier = listPadding.padding(vertical = 4.dp), + block = section, + onItemClick = onExpandClick, + courseSectionsState = courseSectionsState, + courseSubSections = courseSubSections, + downloadedStateMap = uiState.downloadedState, + onSubSectionClick = onSubSectionClick, + onDownloadClick = onDownloadClick + ) } } } @@ -490,6 +455,37 @@ private fun ResumeCourseTablet( } } +@Composable +private fun CourseProgress( + modifier: Modifier = Modifier, + progress: Progress +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(10.dp) + .clip(CircleShape), + progress = progress.value, + color = MaterialTheme.appColors.progressBarColor, + backgroundColor = MaterialTheme.appColors.progressBarBackgroundColor + ) + Text( + text = pluralStringResource( + R.plurals.course_assignments_complete, + progress.assignmentsCompleted, + progress.assignmentsCompleted, + progress.totalAssignmentsCount + ), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.labelSmall + ) + } +} + fun getUnitBlockIcon(block: Block): Int { return when (block.type) { BlockType.VIDEO -> R.drawable.ic_course_video @@ -521,9 +517,7 @@ private fun CourseOutlineScreenPreview() { hasEnded = false ) ), - isCourseNestedListEnabled = true, uiMessage = null, - onItemClick = {}, onExpandClick = {}, onSubSectionClick = {}, onResumeClick = {}, @@ -556,9 +550,7 @@ private fun CourseOutlineScreenTabletPreview() { hasEnded = false ) ), - isCourseNestedListEnabled = true, uiMessage = null, - onItemClick = {}, onExpandClick = {}, onSubSectionClick = {}, onResumeClick = {}, @@ -578,6 +570,11 @@ private fun ResumeCoursePreview() { } } +private val mockAssignmentProgress = AssignmentProgress( + assignmentType = "Home", + numPointsEarned = 1f, + numPointsPossible = 3f +) private val mockChapterBlock = Block( id = "id", blockId = "blockId", @@ -593,7 +590,9 @@ private val mockChapterBlock = Block( descendants = emptyList(), descendantsType = BlockType.CHAPTER, completion = 0.0, - containsGatedContent = false + containsGatedContent = false, + assignmentProgress = mockAssignmentProgress, + due = Date() ) private val mockSequentialBlock = Block( id = "id", @@ -610,7 +609,9 @@ private val mockSequentialBlock = Block( descendants = emptyList(), descendantsType = BlockType.CHAPTER, completion = 0.0, - containsGatedContent = false + containsGatedContent = false, + assignmentProgress = mockAssignmentProgress, + due = Date() ) private val mockCourseStructure = CourseStructure( @@ -634,5 +635,6 @@ private val mockCourseStructure = CourseStructure( ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = Progress(1, 3) ) diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt index 11ec94d95..602c7d2fa 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt @@ -64,7 +64,7 @@ class CourseOutlineViewModel( workerController, coreAnalytics ) { - val isCourseNestedListEnabled get() = config.getCourseUIConfig().isCourseNestedListEnabled + val isCourseNestedListEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled private val _uiState = MutableStateFlow(CourseOutlineUIState.Loading) val uiState: StateFlow @@ -81,7 +81,7 @@ class CourseOutlineViewModel( private var resumeSectionBlock: Block? = null private var resumeVerticalBlock: Block? = null - private val isCourseExpandableSectionsEnabled get() = config.getCourseUIConfig().isCourseNestedListEnabled + private val isCourseExpandableSectionsEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled private val courseSubSections = mutableMapOf>() private val subSectionsDownloadsCount = mutableMapOf() @@ -239,17 +239,12 @@ class CourseOutlineViewModel( resultBlocks.add(block) block.descendants.forEach { descendant -> blocks.find { it.id == descendant }?.let { sequentialBlock -> - if (isCourseNestedListEnabled) { - courseSubSections.getOrPut(block.id) { mutableListOf() } - .add(sequentialBlock) - courseSubSectionUnit[sequentialBlock.id] = - sequentialBlock.getFirstDescendantBlock(blocks) - subSectionsDownloadsCount[sequentialBlock.id] = - sequentialBlock.getDownloadsCount(blocks) - - } else { - resultBlocks.add(sequentialBlock) - } + courseSubSections.getOrPut(block.id) { mutableListOf() } + .add(sequentialBlock) + courseSubSectionUnit[sequentialBlock.id] = + sequentialBlock.getFirstDescendantBlock(blocks) + subSectionsDownloadsCount[sequentialBlock.id] = + sequentialBlock.getDownloadsCount(blocks) addDownloadableChildrenForSequentialBlock(sequentialBlock) } } diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt index 6a1a1bf9e..0c83b264b 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt @@ -61,6 +61,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.BlockType import org.openedx.core.UIMessage +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.extension.serializable @@ -78,10 +79,13 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue +import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.FileUtil import org.openedx.course.R import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.CardArrow +import java.io.File +import java.util.Date import org.openedx.core.R as CoreR class CourseSectionFragment : Fragment() { @@ -476,5 +480,7 @@ private val mockBlock = Block( descendants = emptyList(), descendantsType = BlockType.HTML, completion = 0.0, - containsGatedContent = false + containsGatedContent = false, + assignmentProgress = AssignmentProgress("", 1f, 2f), + due = Date() ) diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index 6a89c1dc2..27da57afb 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -1,8 +1,10 @@ package org.openedx.course.presentation.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image @@ -44,12 +46,15 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.CloudDone import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -58,7 +63,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -72,6 +79,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import org.jsoup.Jsoup import org.openedx.core.BlockType +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseDatesBannerInfo @@ -90,6 +98,7 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.core.utils.TimeUtils import org.openedx.course.R import org.openedx.course.presentation.dates.mockedCourseBannerInfo import org.openedx.course.presentation.outline.getUnitBlockIcon @@ -276,7 +285,7 @@ fun CardArrow( ) { Icon( imageVector = Icons.Filled.ChevronRight, - tint = MaterialTheme.appColors.primary, + tint = MaterialTheme.appColors.textDark, contentDescription = "Expandable Arrow", modifier = Modifier.rotate(degrees), ) @@ -573,81 +582,181 @@ fun VideoSubtitles( } @Composable -fun CourseExpandableChapterCard( - modifier: Modifier, +fun CourseSection( + modifier: Modifier = Modifier, block: Block, onItemClick: (Block) -> Unit, + courseSectionsState: Boolean?, + courseSubSections: List?, + downloadedStateMap: Map, + onSubSectionClick: (Block) -> Unit, + onDownloadClick: (blocksIds: List) -> Unit, +) { + val arrowRotation by animateFloatAsState( + targetValue = if (courseSectionsState == true) -90f else 90f, label = "" + ) + val sectionIds = courseSubSections?.map { it.id }.orEmpty() + val filteredStatuses = downloadedStateMap.filterKeys { it in sectionIds }.values + val downloadedState = when { + filteredStatuses.isEmpty() -> null + filteredStatuses.all { it.isDownloaded } -> DownloadedState.DOWNLOADED + filteredStatuses.any { it.isWaitingOrDownloading } -> DownloadedState.DOWNLOADING + else -> DownloadedState.NOT_DOWNLOADED + } + + Column(modifier = modifier + .clip(MaterialTheme.appShapes.cardShape) + .noRippleClickable { onItemClick(block) } + .background(MaterialTheme.appColors.cardViewBackground) + .border( + 1.dp, + MaterialTheme.appColors.cardViewBorder, + MaterialTheme.appShapes.cardShape + ) + ) { + CourseExpandableChapterCard( + block = block, + arrowDegrees = arrowRotation, + downloadedState = downloadedState, + onDownloadClick = { + onDownloadClick(downloadedStateMap.keys.toList()) + } + ) + courseSubSections?.forEach { subSectionBlock -> + AnimatedVisibility( + visible = courseSectionsState == true + ) { + CourseSubSectionItem( + block = subSectionBlock, + onClick = onSubSectionClick + ) + } + } + } +} + +@Composable +fun CourseExpandableChapterCard( + modifier: Modifier = Modifier, + block: Block, arrowDegrees: Float = 0f, + downloadedState: DownloadedState?, + onDownloadClick: () -> Unit, ) { - Column(modifier = Modifier - .clickable { onItemClick(block) } - .background(if (block.isCompleted()) MaterialTheme.appColors.surface else Color.Transparent) + val iconModifier = Modifier.size(24.dp) + Row( + modifier + .fillMaxWidth() + .height(48.dp) + .padding(vertical = 8.dp) + .padding(start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { + CardArrow(degrees = arrowDegrees) + if (block.isCompleted()) { + val completedIconPainter = painterResource(R.drawable.course_ic_task_alt) + val completedIconColor = MaterialTheme.appColors.successGreen + val completedIconDescription = stringResource(id = R.string.course_accessibility_section_completed) + + Icon( + painter = completedIconPainter, + contentDescription = completedIconDescription, + tint = completedIconColor + ) + } + Text( + modifier = Modifier.weight(1f), + text = block.displayName, + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) Row( - modifier - .fillMaxWidth() - .height(60.dp) - .padding( - vertical = 8.dp - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + modifier = Modifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically ) { - if (block.isCompleted()) { - val completedIconPainter = painterResource(R.drawable.course_ic_task_alt) - val completedIconColor = MaterialTheme.appColors.primary - val completedIconDescription = - stringResource(id = R.string.course_accessibility_section_completed) - - Icon( - painter = completedIconPainter, - contentDescription = completedIconDescription, - tint = completedIconColor - ) - Spacer(modifier = Modifier.width(16.dp)) + if (downloadedState == DownloadedState.DOWNLOADED || downloadedState == DownloadedState.NOT_DOWNLOADED) { + val downloadIconPainter = + if (downloadedState == DownloadedState.DOWNLOADED) { + rememberVectorPainter(Icons.Default.CloudDone) + } else { + painterResource(id = R.drawable.course_ic_start_download) + } + val downloadIconDescription = + if (downloadedState == DownloadedState.DOWNLOADED) { + stringResource(id = R.string.course_accessibility_remove_course_section) + } else { + stringResource(id = R.string.course_accessibility_download_course_section) + } + val downloadIconTint = + if (downloadedState == DownloadedState.DOWNLOADED) { + MaterialTheme.appColors.successGreen + } else { + MaterialTheme.appColors.textAccent + } + IconButton(modifier = iconModifier, + onClick = { onDownloadClick() }) { + Icon( + painter = downloadIconPainter, + contentDescription = downloadIconDescription, + tint = downloadIconTint + ) + } + } else if (downloadedState != null) { + Box(contentAlignment = Alignment.Center) { + if (downloadedState == DownloadedState.DOWNLOADING) { + CircularProgressIndicator( + modifier = Modifier.size(28.dp), + backgroundColor = Color.LightGray, + strokeWidth = 2.dp, + color = MaterialTheme.appColors.primary + ) + } else if (downloadedState == DownloadedState.WAITING) { + Icon( + painter = painterResource(id = R.drawable.course_download_waiting), + contentDescription = stringResource(id = R.string.course_accessibility_stop_downloading_course_section), + tint = MaterialTheme.appColors.error + ) + } + IconButton( + modifier = iconModifier.padding(2.dp), + onClick = { onDownloadClick() }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(id = R.string.course_accessibility_stop_downloading_course_section), + tint = MaterialTheme.appColors.error + ) + } + } } - Text( - modifier = Modifier.weight(1f), - text = block.displayName, - style = MaterialTheme.appTypography.titleSmall, - color = MaterialTheme.appColors.textPrimary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.width(16.dp)) - CardArrow(degrees = arrowDegrees) } } } @Composable fun CourseSubSectionItem( - modifier: Modifier, + modifier: Modifier = Modifier, block: Block, - downloadedState: DownloadedState?, - downloadsCount: Int, onClick: (Block) -> Unit, - onDownloadClick: (Block) -> Unit, ) { + val context = LocalContext.current val icon = - if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource( - coreR.drawable.ic_core_chapter_icon - ) + if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource(coreR.drawable.ic_core_chapter_icon) val iconColor = - if (block.isCompleted()) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface - - val iconModifier = Modifier.size(24.dp) - - Column(Modifier - .clickable { onClick(block) } - .background(if (block.isCompleted()) MaterialTheme.appColors.surface else Color.Transparent) + if (block.isCompleted()) MaterialTheme.appColors.successGreen else MaterialTheme.appColors.onSurface + val due by rememberSaveable { + mutableStateOf(block.due?.let { TimeUtils.getAssignmentFormattedDate(context, it) }) + } + val isAssignmentEnable = !block.isCompleted() && block.assignmentProgress != null && !due.isNullOrEmpty() + Column( + modifier = modifier + .fillMaxWidth() + .clickable { onClick(block) } + .padding(horizontal = 16.dp, vertical = 12.dp) ) { Row( - modifier - .fillMaxWidth() - .height(60.dp) - .padding(vertical = 16.dp) - .padding(start = 20.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { @@ -656,7 +765,7 @@ fun CourseSubSectionItem( contentDescription = null, tint = iconColor ) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(4.dp)) Text( modifier = Modifier.weight(1f), text = block.displayName, @@ -666,63 +775,31 @@ fun CourseSubSectionItem( maxLines = 1 ) Spacer(modifier = Modifier.width(16.dp)) - Row( - modifier = Modifier.fillMaxHeight(), - horizontalArrangement = Arrangement.spacedBy(if (downloadsCount > 0) 8.dp else 24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - if (downloadedState == DownloadedState.DOWNLOADED || downloadedState == DownloadedState.NOT_DOWNLOADED) { - val downloadIconPainter = if (downloadedState == DownloadedState.DOWNLOADED) { - painterResource(id = R.drawable.course_ic_remove_download) - } else { - painterResource(id = R.drawable.course_ic_start_download) - } - val downloadIconDescription = - if (downloadedState == DownloadedState.DOWNLOADED) { - stringResource(id = R.string.course_accessibility_remove_course_section) - } else { - stringResource(id = R.string.course_accessibility_download_course_section) - } - IconButton(modifier = iconModifier, - onClick = { onDownloadClick(block) }) { - Icon( - painter = downloadIconPainter, - contentDescription = downloadIconDescription, - tint = MaterialTheme.appColors.textPrimary - ) - } - } else if (downloadedState != null) { - Box(contentAlignment = Alignment.Center) { - if (downloadedState == DownloadedState.DOWNLOADING || downloadedState == DownloadedState.WAITING) { - CircularProgressIndicator( - modifier = Modifier.size(28.dp), - backgroundColor = Color.LightGray, - strokeWidth = 2.dp, - color = MaterialTheme.appColors.primary - ) - } - IconButton( - modifier = iconModifier.padding(2.dp), - onClick = { onDownloadClick(block) }) { - Text( - modifier = Modifier - .padding(bottom = 4.dp), - text = "i", - style = MaterialTheme.appTypography.titleMedium, - color = MaterialTheme.appColors.primary - ) - } - } - } - if (downloadsCount > 0) { - Text( - text = downloadsCount.toString(), - style = MaterialTheme.appTypography.titleSmall, - color = MaterialTheme.appColors.textPrimary - ) - } + if (isAssignmentEnable) { + Icon( + imageVector = Icons.Filled.ChevronRight, + tint = MaterialTheme.appColors.onSurface, + contentDescription = null + ) } } + + if (isAssignmentEnable) { + val assignmentString = + stringResource( + R.string.course_subsection_assignment_info, + block.assignmentProgress?.assignmentType ?: "", + due ?: "", + block.assignmentProgress?.numPointsEarned?.toInt() ?: 0, + block.assignmentProgress?.numPointsPossible?.toInt() ?: 0 + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = assignmentString, + style = MaterialTheme.appTypography.bodySmall, + color = MaterialTheme.appColors.textPrimary + ) + } } } @@ -1146,7 +1223,7 @@ private fun NavigationUnitsButtonsWithNextPreview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun CourseChapterItemPreview() { +private fun CourseSectionCardPreview() { OpenEdXTheme { Surface(color = MaterialTheme.appColors.background) { CourseSectionCard( @@ -1246,5 +1323,7 @@ private val mockChapterBlock = Block( descendants = emptyList(), descendantsType = BlockType.CHAPTER, completion = 0.0, - containsGatedContent = false + containsGatedContent = false, + assignmentProgress = AssignmentProgress("", 1f, 2f), + due = Date() ) diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt index 6e8f19610..7beb0f91d 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt @@ -1,7 +1,6 @@ package org.openedx.course.presentation.ui import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -19,7 +18,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.AlertDialog @@ -59,10 +57,12 @@ import androidx.fragment.app.FragmentManager import org.openedx.core.AppDataConstants import org.openedx.core.BlockType import org.openedx.core.UIMessage +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.domain.model.Progress import org.openedx.core.domain.model.VideoSettings import org.openedx.core.extension.toFileSize import org.openedx.core.module.download.DownloadModelsSize @@ -98,7 +98,6 @@ fun CourseVideosScreen( uiState = uiState, uiMessage = uiMessage, courseTitle = viewModel.courseTitle, - isCourseNestedListEnabled = viewModel.isCourseNestedListEnabled, videoSettings = videoSettings, onItemClick = { block -> viewModel.courseRouter.navigateToCourseSubsections( @@ -125,20 +124,12 @@ fun CourseVideosScreen( ) } }, - onDownloadClick = { - if (viewModel.isBlockDownloading(it.id)) { - viewModel.courseRouter.navigateToDownloadQueue( - fm = fragmentManager, - viewModel.getDownloadableChildren(it.id) - ?: arrayListOf() - ) - } else if (viewModel.isBlockDownloaded(it.id)) { - viewModel.removeDownloadModels(it.id) - } else { - viewModel.saveDownloadModels( - FileUtil(context).getExternalAppDir().path, it.id - ) - } + onDownloadClick = { blocksIds -> + viewModel.downloadBlocks( + blocksIds = blocksIds, + fragmentManager = fragmentManager, + context = context + ) }, onDownloadAllClick = { isAllBlocksDownloadedOrDownloading -> viewModel.logBulkDownloadToggleEvent(!isAllBlocksDownloadedOrDownloading) @@ -174,12 +165,11 @@ private fun CourseVideosUI( uiState: CourseVideosUIState, uiMessage: UIMessage?, courseTitle: String, - isCourseNestedListEnabled: Boolean, videoSettings: VideoSettings, onItemClick: (Block) -> Unit, onExpandClick: (Block) -> Unit, onSubSectionClick: (Block) -> Unit, - onDownloadClick: (Block) -> Unit, + onDownloadClick: (blocksIds: List) -> Unit, onDownloadAllClick: (Boolean) -> Unit, onDownloadQueueClick: () -> Unit, onVideoDownloadQualityClick: () -> Unit @@ -294,88 +284,26 @@ private fun CourseVideosUI( } } - if (isCourseNestedListEnabled) { - uiState.courseStructure.blockData.forEach { section -> - val courseSubSections = uiState.courseSubSections[section.id] - val courseSectionsState = uiState.courseSectionsState[section.id] - - item { - Column { - CourseExpandableChapterCard( - modifier = listPadding, - block = section, - onItemClick = onExpandClick, - arrowDegrees = if (courseSectionsState == true) -90f else 90f - ) - Divider() - } - } - - courseSubSections?.forEach { subSectionBlock -> - item { - Column { - AnimatedVisibility( - visible = courseSectionsState == true - ) { - Column { - val downloadsCount = - uiState.subSectionsDownloadsCount[subSectionBlock.id] - ?: 0 - - CourseSubSectionItem( - modifier = listPadding, - block = subSectionBlock, - downloadedState = uiState.downloadedState[subSectionBlock.id], - downloadsCount = downloadsCount, - onClick = onSubSectionClick, - onDownloadClick = { block -> - if (uiState.downloadedState[block.id]?.isDownloaded == true) { - deleteDownloadBlock = - block - - } else { - onDownloadClick(block) - } - } - ) - Divider() - } - } - } - } - } - } - return@LazyColumn + item { + Spacer(modifier = Modifier.height(12.dp)) } + uiState.courseStructure.blockData.forEach { section -> + val courseSubSections = + uiState.courseSubSections[section.id] + val courseSectionsState = + uiState.courseSectionsState[section.id] - items(uiState.courseStructure.blockData) { block -> - Column(listPadding) { - if (block.type == BlockType.CHAPTER) { - Text( - modifier = Modifier.padding( - top = 36.dp, - bottom = 8.dp - ), - text = block.displayName, - style = MaterialTheme.appTypography.titleMedium, - color = MaterialTheme.appColors.textPrimaryVariant - ) - } else { - CourseSectionCard( - block = block, - downloadedState = uiState.downloadedState[block.id], - onItemClick = onItemClick, - onDownloadClick = { block -> - if (uiState.downloadedState[block.id]?.isDownloaded == true) { - deleteDownloadBlock = block - - } else { - onDownloadClick(block) - } - } - ) - Divider() - } + item { + CourseSection( + modifier = listPadding.padding(vertical = 4.dp), + block = section, + onItemClick = onExpandClick, + courseSectionsState = courseSectionsState, + courseSubSections = courseSubSections, + downloadedStateMap = uiState.downloadedState, + onSubSectionClick = onSubSectionClick, + onDownloadClick = onDownloadClick + ) } } } @@ -497,7 +425,7 @@ private fun CourseVideosUI( TextButton( onClick = { deleteDownloadBlock?.let { block -> - onDownloadClick(block) + onDownloadClick(listOf(block.id)) } deleteDownloadBlock = null } @@ -708,7 +636,6 @@ private fun CourseVideosScreenPreview() { ) ), courseTitle = "", - isCourseNestedListEnabled = false, onItemClick = { }, onExpandClick = { }, onSubSectionClick = { }, @@ -733,7 +660,6 @@ private fun CourseVideosScreenEmptyPreview() { "This course does not include any videos." ), courseTitle = "", - isCourseNestedListEnabled = false, onItemClick = { }, onExpandClick = { }, onSubSectionClick = { }, @@ -769,7 +695,6 @@ private fun CourseVideosScreenTabletPreview() { ) ), courseTitle = "", - isCourseNestedListEnabled = false, onItemClick = { }, onExpandClick = { }, onSubSectionClick = { }, @@ -782,6 +707,11 @@ private fun CourseVideosScreenTabletPreview() { } } +private val mockAssignmentProgress = AssignmentProgress( + assignmentType = "Home", + numPointsEarned = 1f, + numPointsPossible = 3f +) private val mockChapterBlock = Block( id = "id", @@ -798,7 +728,9 @@ private val mockChapterBlock = Block( descendants = emptyList(), descendantsType = BlockType.CHAPTER, completion = 0.0, - containsGatedContent = false + containsGatedContent = false, + assignmentProgress = mockAssignmentProgress, + due = Date() ) private val mockSequentialBlock = Block( @@ -816,7 +748,9 @@ private val mockSequentialBlock = Block( descendants = emptyList(), descendantsType = BlockType.SEQUENTIAL, completion = 0.0, - containsGatedContent = false + containsGatedContent = false, + assignmentProgress = mockAssignmentProgress, + due = Date() ) private val mockCourseStructure = CourseStructure( @@ -840,5 +774,6 @@ private val mockCourseStructure = CourseStructure( ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = Progress(1, 3) ) diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt index 61fc896bf..af4d839e7 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -37,7 +37,7 @@ class CourseUnitContainerViewModel( private val blocks = ArrayList() - val isCourseExpandableSectionsEnabled get() = config.getCourseUIConfig().isCourseNestedListEnabled + val isCourseExpandableSectionsEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled val isCourseUnitProgressEnabled get() = config.getCourseUIConfig().isCourseUnitProgressEnabled diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt index 2cf3d8797..49f3b6120 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt @@ -1,5 +1,7 @@ package org.openedx.course.presentation.videos +import android.content.Context +import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -25,6 +27,7 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.system.notifier.VideoNotifier import org.openedx.core.system.notifier.VideoQualityChanged +import org.openedx.core.utils.FileUtil import org.openedx.course.R import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics @@ -53,7 +56,7 @@ class CourseVideoViewModel( coreAnalytics ) { - val isCourseNestedListEnabled get() = config.getCourseUIConfig().isCourseNestedListEnabled + val isCourseNestedListEnabled get() = config.getCourseUIConfig().isCourseDropdownNavigationEnabled private val _uiState = MutableStateFlow(CourseVideosUIState.Loading) val uiState: StateFlow @@ -199,15 +202,10 @@ class CourseVideoViewModel( resultBlocks.add(block) block.descendants.forEach { descendant -> blocks.find { it.id == descendant }?.let { - if (isCourseNestedListEnabled) { - courseSubSections.getOrPut(block.id) { mutableListOf() } - .add(it) - courseSubSectionUnit[it.id] = it.getFirstDescendantBlock(blocks) - subSectionsDownloadsCount[it.id] = it.getDownloadsCount(blocks) - - } else { - resultBlocks.add(it) - } + courseSubSections.getOrPut(block.id) { mutableListOf() } + .add(it) + courseSubSectionUnit[it.id] = it.getFirstDescendantBlock(blocks) + subSectionsDownloadsCount[it.id] = it.getDownloadsCount(blocks) addDownloadableChildrenForSequentialBlock(it) } } @@ -215,4 +213,21 @@ class CourseVideoViewModel( } return resultBlocks.toList() } + + fun downloadBlocks(blocksIds: List, fragmentManager: FragmentManager, context: Context) { + blocksIds.forEach { blockId -> + if (isBlockDownloading(blockId)) { + courseRouter.navigateToDownloadQueue( + fm = fragmentManager, + getDownloadableChildren(blockId) ?: arrayListOf() + ) + } else if (isBlockDownloaded(blockId)) { + removeDownloadModels(blockId) + } else { + saveDownloadModels( + FileUtil(context).getExternalAppDir().path, blockId + ) + } + } + } } diff --git a/course/src/main/res/drawable/course_download_waiting.png b/course/src/main/res/drawable/course_download_waiting.png new file mode 100644 index 0000000000000000000000000000000000000000..c4a04af69630a5f614eb1b71780d4554038f2043 GIT binary patch literal 1945 zcmV;K2WI$*P)upfI!Tjv0M#aRK#3J5CzRsgI3x&nt4Bw0b+*J4iG66mAl9k}N} z9vWNI`;8?lO#E%-@Z&4#3UAI>-VUe&D4k&MJ`6h!JC9`M>?@`A9{k+unVwTj9$V`0l|Q1C z`kDEViI3#Dx3>C=LMa409S$3lFSIX%z>L8%)9K8#D<3^m8OI?{DRX_uBMXGcsI!4T zL<};)vL0WutNpo{m0?{^a~G#Sg+-0dp?&R*~+OYjcs|pZzq`i3lHb&A!;hc zT_N1C|7o~O1*33+1!!2(o2RG4rYXaBQ;1^0=|8%zwkZ#UV`c5fwNW_1f~C;2#pbmdBQ5x&2eiV^&Y3>`wfJwC(MdXKu&9 zRilRNefS!bvgw0U_0PPXeC^~YMg`7@US&3YaKQct@9_QE=ylq2iCH$rZ}*nEeSfRD zLj?hKxvpwq7Geon+P(txz5mfW#8@aNIM2gqKsewD1skof^5+k>T5oL6_x~Oquv^~0 zodh_v>}yS#;V5pYjQ~Pdpv_OxeO>Q3!p)H7?!he=}T21Uva*n6dcK03_ZWIDz)J?E3G%QF+frk`~~ZA*3o zGw(Y4!A${uGp&vYirPZ7ZkD<-Zu)AJnkz)RIJb1B0?5=il|IH0nfEYR|1V|vy)abZ zIj-8tvvGb4*!Eyx5%Y>87nhuA9=IDex-7&BVIbN#O@m@WE`r4y#rI_*RtWT87}%){ ze0*d5mjiU;&yT}4Eh*6VDl@6o26RY}m}$)c8kp#CWeZ&eVC&8n>ktX&!r z!Q^;gx3H9??yV9;;{V;Ad$}x~Sb;s76&fTTm*v=_6^9sFuAC)hW_h5ArcIb;Dy)w` zVvz}r3`MGys|;{Se9CG&-ngJ6oAISajT$v-tO;13$d&UDK3I_ELr)p_Gps9@fWyqi zchqor=FkFLrwI0UeDeQdSh0!C4kEB9ubZ|)D`G}fq5qehKYgIPrlF0o0wp-=s~si@@){T6l%lV?vvurTUOEJV9X&C4Qi z7tiW6yQ~P@poVjaOaoTj6OrpU^dXA^8{ph4f?0zM5oMFl^h`cp(h9>EhNE;OUK>$z z^6NH|E{{e^y|q;)1)kEX!|W!ZfmqD&d(4naM`TB1m0h6XPBr>>=9Y-qxKbw4y+$4aP{Ou*HO%#GF_G-__}+<`mGv zf>5uPB`l^W%xoO|B6KRex7BPdEKs{_&gf7^2y~4sfXnu}x}E0_FokE&7d~|qpDaTZ zTN3EAtN0LOxK96b5e&yCL{S7X%Pfj*iHH^toxuy=^Ey0dCYZh(XXZE?P35rBWr>$e z24`tbC}TpDZq|A-#Tp?-T4c0mDV2CrR(rqQZ4Ib`z@CLIgzWce;P~@KWX;n>Q_9)M z8oQ0YBKkI#G|)E+lyMD12z}n%;I14>NB6f{Rz?uN@*oWKvZ;N)208%#+<7X09{Qp; z^+i`h^gj&WNU!c-8Rp$eauX~o0{wW$kku>7MsO2tBvH!UJ2R)WeE05z<&yqcFx6kf zsjq_>UWTTnoGbm()V*w0g$G86E+^(ZZmpATpl9;)KmdZsPaShRx(_`n7^U1$w;(F6 zXG)(;>}B9@YD(X5Q`PF7G8j(b1cUdFN&E;fxXz{z=Uh|z?9MT3TcyZ_g%j-lFRdeJ zDU(-J&7iFALTP?kX;lTSi7XyW0(o?{lV4}UP~HBz^QtRo?}?sEd - - - - - - + diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index 3d6e13094..2fcb1d950 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -62,5 +62,15 @@ Turning off the switch will stop downloading and delete all downloaded videos for \"%s\"? Are you sure you want to delete all video(s) for \"%s\"? Are you sure you want to delete video(s) for \"%s\"? + %1$s - %2$s - %3$d / %4$d + + + %1$s of %2$s assignments complete + %1$s of %2$s assignment complete + %1$s of %2$s assignments complete + %1$s of %2$s assignments complete + %1$s of %2$s assignments complete + %1$s of %2$s assignments complete + diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index c20bb07be..938d850d2 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -108,7 +108,8 @@ class CourseContainerViewModelTest { ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = null ) private val courseStructureModel = CourseStructureModel( @@ -125,7 +126,8 @@ class CourseContainerViewModelTest { coursewareAccess = null, media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = null ) @Before @@ -168,12 +170,12 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStructure(any()) } throws UnknownHostException() + coEvery { interactor.getCourseStructure(any(), any()) } throws UnknownHostException() every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any(), any()) } verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } val message = viewModel.errorMessage.value @@ -202,12 +204,12 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStructure(any()) } throws Exception() + coEvery { interactor.getCourseStructure(any(), any()) } throws Exception() every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any(), any()) } verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } val message = viewModel.errorMessage.value @@ -236,12 +238,12 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructure(any(), any()) } returns courseStructure every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any(), any()) } verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } assert(viewModel.errorMessage.value == null) @@ -269,7 +271,7 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns false - coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructure(any(), any()) } returns courseStructure every { analytics.logEvent(any(), any()) } returns Unit coEvery { courseApi.getCourseStructure(any(), any(), any(), any()) diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index 5e2ed50a4..11ffb4932 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -136,6 +136,7 @@ class CourseDatesViewModelTest { media = null, certificate = null, isSelfPaced = true, + progress = null ) @Before diff --git a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt index 941fbfdac..aad650b28 100644 --- a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -33,6 +33,7 @@ import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.data.model.DateType import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseComponentStatus @@ -84,6 +85,12 @@ class CourseOutlineViewModelTest { private val somethingWrong = "Something went wrong" private val cantDownload = "You can download content only from Wi-fi" + private val assignmentProgress = AssignmentProgress( + assignmentType = "Homework", + numPointsEarned = 1f, + numPointsPossible = 3f + ) + private val blocks = listOf( Block( id = "id", @@ -99,7 +106,9 @@ class CourseOutlineViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("1", "id1"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id1", @@ -115,7 +124,9 @@ class CourseOutlineViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("id2"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id2", @@ -131,7 +142,9 @@ class CourseOutlineViewModelTest { blockCounts = BlockCounts(0), descendants = emptyList(), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ) ) @@ -156,7 +169,8 @@ class CourseOutlineViewModelTest { ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = null ) private val dateBlock = CourseDateBlock( @@ -303,7 +317,7 @@ class CourseOutlineViewModelTest { ) } coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseOutlineViewModel( "", @@ -351,7 +365,7 @@ class CourseOutlineViewModelTest { ) } coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseOutlineViewModel( "", @@ -398,7 +412,7 @@ class CourseOutlineViewModelTest { ) } coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseOutlineViewModel( "", @@ -482,7 +496,7 @@ class CourseOutlineViewModelTest { coEvery { workerController.saveModels(any()) } returns Unit coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") coEvery { downloadDao.readAllData() } returns flow { emit(emptyList()) } - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseOutlineViewModel( "", @@ -525,7 +539,7 @@ class CourseOutlineViewModelTest { every { networkConnection.isOnline() } returns true coEvery { workerController.saveModels(any()) } returns Unit coEvery { downloadDao.readAllData() } returns flow { emit(emptyList()) } - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { coreAnalytics.logEvent(any(), any()) } returns Unit val viewModel = CourseOutlineViewModel( @@ -562,7 +576,7 @@ class CourseOutlineViewModelTest { every { networkConnection.isOnline() } returns false coEvery { workerController.saveModels(any()) } returns Unit coEvery { downloadDao.readAllData() } returns flow { emit(emptyList()) } - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseOutlineViewModel( "", diff --git a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt index 0a398371b..01c685c48 100644 --- a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt @@ -27,6 +27,7 @@ import org.openedx.core.BlockType import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseStructure @@ -69,6 +70,11 @@ class CourseSectionViewModelTest { private val somethingWrong = "Something went wrong" private val cantDownload = "You can download content only from Wi-fi" + private val assignmentProgress = AssignmentProgress( + assignmentType = "Homework", + numPointsEarned = 1f, + numPointsPossible = 3f + ) private val blocks = listOf( Block( @@ -85,7 +91,9 @@ class CourseSectionViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("1", "id1"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id1", @@ -101,7 +109,9 @@ class CourseSectionViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("id2"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id2", @@ -117,7 +127,9 @@ class CourseSectionViewModelTest { blockCounts = BlockCounts(0), descendants = emptyList(), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ) ) @@ -142,7 +154,8 @@ class CourseSectionViewModelTest { ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = null ) private val downloadModel = DownloadModel( diff --git a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt index 1e5354a95..166d7751e 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt @@ -20,6 +20,7 @@ import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.BlockType import org.openedx.core.config.Config +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseStructure @@ -44,6 +45,12 @@ class CourseUnitContainerViewModelTest { private val notifier = mockk() private val analytics = mockk() + private val assignmentProgress = AssignmentProgress( + assignmentType = "Homework", + numPointsEarned = 1f, + numPointsPossible = 3f + ) + private val blocks = listOf( Block( id = "id", @@ -59,7 +66,9 @@ class CourseUnitContainerViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("id2", "id1"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id1", @@ -75,7 +84,9 @@ class CourseUnitContainerViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("id2", "id"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id2", @@ -91,7 +102,9 @@ class CourseUnitContainerViewModelTest { blockCounts = BlockCounts(0), descendants = emptyList(), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id3", @@ -107,7 +120,9 @@ class CourseUnitContainerViewModelTest { blockCounts = BlockCounts(0), descendants = emptyList(), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ) ) @@ -133,7 +148,8 @@ class CourseUnitContainerViewModelTest { ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = null ) @Before diff --git a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt index a15de0583..9bb8d0f5f 100644 --- a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt @@ -32,6 +32,7 @@ import org.openedx.core.BlockType import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseStructure @@ -78,6 +79,12 @@ class CourseVideoViewModelTest { private val cantDownload = "You can download content only from Wi-fi" + private val assignmentProgress = AssignmentProgress( + assignmentType = "Homework", + numPointsEarned = 1f, + numPointsPossible = 3f + ) + private val blocks = listOf( Block( id = "id", @@ -93,7 +100,9 @@ class CourseVideoViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("1", "id1"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id1", @@ -109,7 +118,9 @@ class CourseVideoViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("id2"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id2", @@ -125,7 +136,9 @@ class CourseVideoViewModelTest { blockCounts = BlockCounts(0), descendants = emptyList(), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ) ) @@ -150,7 +163,8 @@ class CourseVideoViewModelTest { ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = null ) private val downloadModelEntity = @@ -183,7 +197,7 @@ class CourseVideoViewModelTest { @Test fun `getVideos empty list`() = runTest { - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure.copy(blockData = emptyList()) every { downloadDao.readAllData() } returns flow { emit(emptyList()) } @@ -215,7 +229,7 @@ class CourseVideoViewModelTest { @Test fun `getVideos success`() = runTest { - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure every { downloadDao.readAllData() } returns flow { emit(emptyList()) } every { preferencesManager.videoSettings } returns VideoSettings.default @@ -248,7 +262,7 @@ class CourseVideoViewModelTest { @Test fun `updateVideos success`() = runTest { - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { courseNotifier.notifier } returns flow { emit(CourseStructureUpdated("")) @@ -291,7 +305,7 @@ class CourseVideoViewModelTest { @Test fun `setIsUpdating success`() = runTest { - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { downloadDao.readAllData() } returns flow { emit(listOf(downloadModelEntity)) } @@ -300,7 +314,7 @@ class CourseVideoViewModelTest { @Test fun `saveDownloadModels test`() = runTest(UnconfinedTestDispatcher()) { - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( "", @@ -337,7 +351,7 @@ class CourseVideoViewModelTest { @Test fun `saveDownloadModels only wifi download, with connection`() = runTest(UnconfinedTestDispatcher()) { - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( "", @@ -378,7 +392,7 @@ class CourseVideoViewModelTest { @Test fun `saveDownloadModels only wifi download, without connection`() = runTest(UnconfinedTestDispatcher()) { - every { config.getCourseUIConfig().isCourseNestedListEnabled } returns false + every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( "", diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index c4ea029b9..74515e0c1 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -564,7 +565,11 @@ private fun PrimaryCourseCard( }, painter = rememberVectorPainter(Icons.Default.Warning), title = title, - info = stringResource(R.string.dashboard_past_due_assignment, pastAssignments.size) + info = pluralStringResource( + R.plurals.dashboard_past_due_assignment, + pastAssignments.size, + pastAssignments.size + ) ) } val futureAssignments = primaryCourse.courseAssignments?.futureAssignments @@ -583,9 +588,9 @@ private fun PrimaryCourseCard( painter = painterResource(id = CoreR.drawable.ic_core_chapter_icon), title = title, info = stringResource( - R.string.dashboard_assignment_due_in_days, + R.string.dashboard_assignment_due, nearestAssignment.assignmentType ?: "", - TimeUtils.getCourseFormattedDate(context, nearestAssignment.date) + TimeUtils.getAssignmentFormattedDate(context, nearestAssignment.date) ) ) } @@ -769,7 +774,7 @@ private fun NoCoursesInfo( private val mockCourseDateBlock = CourseDateBlock( title = "Homework 1: ABCD", description = "After this date, course content will be archived", - date = TimeUtils.iso8601ToDate("2023-10-20T15:08:07Z")!!, + date = TimeUtils.iso8601ToDate("2024-05-31T15:08:07Z")!!, assignmentType = "Homework" ) private val mockCourseAssignments = diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index 4ca0c4fce..23a33fb50 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -9,10 +9,9 @@ Course %1$s Start Course Resume Course - %1$d Past Due Assignments View All Courses (%1$d) View All - %1$s Due in %2$s + %1$s %2$s All In Progress Completed @@ -22,4 +21,13 @@ You are not currently enrolled in any courses, would you like to explore the course catalog? Find a Course No %1$s Courses + + + %1$d Past Due Assignments + %1$d Past Due Assignment + %1$d Past Due Assignments + %1$d Past Due Assignments + %1$d Past Due Assignments + %1$d Past Due Assignments + diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 8c08df7f6..8b1dc8f07 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -82,4 +82,3 @@ SOCIAL_AUTH_ENABLED: false UI_COMPONENTS: COURSE_NESTED_LIST_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false - diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt index b74cc6644..9fc56f6af 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt @@ -26,6 +26,7 @@ import org.junit.rules.TestRule import org.openedx.core.BlockType import org.openedx.core.R import org.openedx.core.UIMessage +import org.openedx.core.domain.model.AssignmentProgress import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseStructure @@ -56,6 +57,12 @@ class DiscussionTopicsViewModelTest { private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" + private val assignmentProgress = AssignmentProgress( + assignmentType = "Homework", + numPointsEarned = 1f, + numPointsPossible = 3f + ) + private val blocks = listOf( Block( id = "id", @@ -71,7 +78,9 @@ class DiscussionTopicsViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("1", "id1"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id1", @@ -87,7 +96,9 @@ class DiscussionTopicsViewModelTest { blockCounts = BlockCounts(0), descendants = listOf("id2"), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ), Block( id = "id2", @@ -103,7 +114,9 @@ class DiscussionTopicsViewModelTest { blockCounts = BlockCounts(0), descendants = emptyList(), descendantsType = BlockType.HTML, - completion = 0.0 + completion = 0.0, + assignmentProgress = assignmentProgress, + due = Date() ) ) private val courseStructure = CourseStructure( @@ -127,7 +140,8 @@ class DiscussionTopicsViewModelTest { ), media = null, certificate = null, - isSelfPaced = false + isSelfPaced = false, + progress = null ) @Before diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt index 419172232..f94064d30 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt @@ -518,7 +518,7 @@ private fun AppVersionItemAppToDate(versionName: String) { ), painter = painterResource(id = R.drawable.core_ic_check), contentDescription = null, - tint = MaterialTheme.appColors.accessGreen + tint = MaterialTheme.appColors.successGreen ) Text( modifier = Modifier.testTag("txt_up_to_date"),