Skip to content

Commit 5ac6548

Browse files
feat: course completion pager tab
1 parent f7cfb43 commit 5ac6548

File tree

14 files changed

+523
-387
lines changed

14 files changed

+523
-387
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.openedx.core
2+
3+
import org.openedx.core.domain.model.AssignmentProgress
4+
import org.openedx.core.domain.model.Block
5+
import org.openedx.core.domain.model.BlockCounts
6+
import org.openedx.core.domain.model.CourseStructure
7+
import org.openedx.core.domain.model.CoursewareAccess
8+
import org.openedx.core.domain.model.OfflineDownload
9+
import org.openedx.core.domain.model.Progress
10+
import java.util.Date
11+
12+
object Mock {
13+
private val mockAssignmentProgress = AssignmentProgress(
14+
assignmentType = "Home",
15+
numPointsEarned = 1f,
16+
numPointsPossible = 3f,
17+
shortLabel = "HM1"
18+
)
19+
val mockChapterBlock = Block(
20+
id = "id",
21+
blockId = "blockId",
22+
lmsWebUrl = "lmsWebUrl",
23+
legacyWebUrl = "legacyWebUrl",
24+
studentViewUrl = "studentViewUrl",
25+
type = BlockType.CHAPTER,
26+
displayName = "Chapter",
27+
graded = false,
28+
studentViewData = null,
29+
studentViewMultiDevice = false,
30+
blockCounts = BlockCounts(1),
31+
descendants = emptyList(),
32+
descendantsType = BlockType.CHAPTER,
33+
completion = 0.0,
34+
containsGatedContent = false,
35+
assignmentProgress = mockAssignmentProgress,
36+
due = Date(),
37+
offlineDownload = null
38+
)
39+
private val mockSequentialBlock = Block(
40+
id = "id",
41+
blockId = "blockId",
42+
lmsWebUrl = "lmsWebUrl",
43+
legacyWebUrl = "legacyWebUrl",
44+
studentViewUrl = "studentViewUrl",
45+
type = BlockType.SEQUENTIAL,
46+
displayName = "Sequential",
47+
graded = false,
48+
studentViewData = null,
49+
studentViewMultiDevice = false,
50+
blockCounts = BlockCounts(1),
51+
descendants = emptyList(),
52+
descendantsType = BlockType.CHAPTER,
53+
completion = 0.0,
54+
containsGatedContent = false,
55+
assignmentProgress = mockAssignmentProgress,
56+
due = Date(),
57+
offlineDownload = OfflineDownload("fileUrl", "", 1),
58+
)
59+
60+
val mockCourseStructure = CourseStructure(
61+
root = "",
62+
blockData = listOf(mockSequentialBlock, mockSequentialBlock),
63+
id = "id",
64+
name = "Course name",
65+
number = "",
66+
org = "Org",
67+
start = Date(),
68+
startDisplay = "",
69+
startType = "",
70+
end = Date(),
71+
coursewareAccess = CoursewareAccess(
72+
true,
73+
"",
74+
"",
75+
"",
76+
"",
77+
""
78+
),
79+
media = null,
80+
certificate = null,
81+
isSelfPaced = false,
82+
progress = Progress(1, 3),
83+
)
84+
}

core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ class CourseNotifier {
2323
suspend fun send(event: RefreshDates) = channel.emit(event)
2424
suspend fun send(event: RefreshDiscussions) = channel.emit(event)
2525
suspend fun send(event: RefreshProgress) = channel.emit(event)
26+
suspend fun send(event: CourseProgressLoaded) = channel.emit(event)
2627
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.openedx.core.system.notifier
2+
3+
object CourseProgressLoaded : CourseEvent

course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,10 @@ class CourseRepository(
264264
trySend(cached.mapToDomain())
265265
}
266266
}
267-
val response = api.getCourseProgress(courseId)
268-
courseDao.insertCourseProgressEntity(response.mapToRoomEntity(courseId))
269-
trySend(response.mapToDomain())
267+
if (networkConnection.isOnline()) {
268+
val response = api.getCourseProgress(courseId)
269+
courseDao.insertCourseProgressEntity(response.mapToRoomEntity(courseId))
270+
trySend(response.mapToDomain())
271+
}
270272
}
271273
}

course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,18 @@ private fun DashboardPager(
478478
fragmentManager = fragmentManager,
479479
onResetDatesClick = {
480480
viewModel.onRefresh(CourseContainerTab.DATES)
481+
},
482+
onNavigateToContent = { contentTab ->
483+
scope.launch {
484+
// First scroll to CONTENT tab
485+
pagerState.animateScrollToPage(
486+
CourseContainerTab.entries.indexOf(CourseContainerTab.CONTENT)
487+
)
488+
// Then scroll to the specified content tab
489+
contentTabPagerState.animateScrollToPage(
490+
CourseContentTab.entries.indexOf(contentTab)
491+
)
492+
}
481493
}
482494
)
483495
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package org.openedx.course.presentation.home
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.layout.size
11+
import androidx.compose.foundation.layout.width
12+
import androidx.compose.material.Icon
13+
import androidx.compose.material.MaterialTheme
14+
import androidx.compose.material.Text
15+
import androidx.compose.material.TextButton
16+
import androidx.compose.material.icons.Icons
17+
import androidx.compose.material.icons.automirrored.filled.List
18+
import androidx.compose.runtime.Composable
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.res.stringResource
22+
import androidx.compose.ui.text.font.FontWeight
23+
import androidx.compose.ui.tooling.preview.Preview
24+
import androidx.compose.ui.unit.dp
25+
import org.openedx.core.Mock
26+
import org.openedx.core.domain.model.Block
27+
import org.openedx.core.domain.model.CourseDatesBannerInfo
28+
import org.openedx.core.ui.theme.OpenEdXTheme
29+
import org.openedx.core.ui.theme.appColors
30+
import org.openedx.core.ui.theme.appTypography
31+
import org.openedx.course.R
32+
import org.openedx.course.presentation.progress.CourseCompletionCircularProgress
33+
import org.openedx.course.presentation.ui.CourseSection
34+
35+
@Composable
36+
fun CourseCompletionHomePagerCardContent(
37+
modifier: Modifier = Modifier,
38+
uiState: CourseHomeUIState.CourseData,
39+
onViewAllContentClick: () -> Unit,
40+
onDownloadClick: (blockIds: List<String>) -> Unit,
41+
onSubSectionClick: (Block) -> Unit,
42+
) {
43+
val courseProgress = uiState.courseProgress?.completion ?: 0f
44+
val courseProgressPercent = uiState.courseProgress?.completionPercent ?: 0
45+
46+
Column(
47+
modifier = modifier
48+
.fillMaxWidth()
49+
.padding(16.dp)
50+
) {
51+
// Title
52+
Text(
53+
text = stringResource(R.string.course_completion_title),
54+
style = MaterialTheme.appTypography.titleLarge,
55+
color = MaterialTheme.appColors.textDark
56+
)
57+
58+
Spacer(modifier = Modifier.height(12.dp))
59+
60+
// Progress Section
61+
Row(
62+
modifier = Modifier.fillMaxWidth(),
63+
horizontalArrangement = Arrangement.SpaceBetween,
64+
verticalAlignment = Alignment.CenterVertically
65+
) {
66+
Column(
67+
modifier = Modifier.weight(1f),
68+
verticalArrangement = Arrangement.spacedBy(8.dp)
69+
) {
70+
Text(
71+
text = stringResource(R.string.course_completion_progress_label),
72+
style = MaterialTheme.appTypography.labelLarge,
73+
color = MaterialTheme.appColors.textDark,
74+
fontWeight = FontWeight.Bold
75+
)
76+
Text(
77+
text = stringResource(
78+
R.string.course_completion_progress_description,
79+
courseProgressPercent
80+
),
81+
style = MaterialTheme.appTypography.bodyMedium,
82+
color = MaterialTheme.appColors.textDark
83+
)
84+
}
85+
86+
// Circular Progress
87+
CourseCompletionCircularProgress(
88+
progress = courseProgress,
89+
progressPercent = courseProgressPercent,
90+
completedText = stringResource(R.string.course_completion_completed)
91+
)
92+
}
93+
94+
Spacer(modifier = Modifier.height(16.dp))
95+
96+
uiState.next?.let { (chapter, subsection) ->
97+
// Section progress
98+
val subSections = uiState.courseSubSections[chapter.id]
99+
val completedCount = subSections?.count { it.isCompleted() } ?: 0
100+
val totalCount = subSections?.size ?: 0
101+
val progress = if (totalCount > 0) completedCount.toFloat() / totalCount else 0f
102+
103+
CourseSection(
104+
section = chapter,
105+
onItemClick = {},
106+
isExpandable = false,
107+
isSectionVisible = true,
108+
useRelativeDates = uiState.useRelativeDates,
109+
subSections = listOf(subsection),
110+
downloadedStateMap = uiState.downloadedState,
111+
onSubSectionClick = onSubSectionClick,
112+
onDownloadClick = onDownloadClick,
113+
progress = progress
114+
)
115+
}
116+
117+
Spacer(modifier = Modifier.height(8.dp))
118+
119+
// View All Content Button
120+
TextButton(
121+
onClick = onViewAllContentClick,
122+
modifier = Modifier.align(Alignment.CenterHorizontally)
123+
) {
124+
Row(
125+
verticalAlignment = Alignment.CenterVertically,
126+
horizontalArrangement = Arrangement.Center
127+
) {
128+
Icon(
129+
imageVector = Icons.AutoMirrored.Filled.List,
130+
contentDescription = null,
131+
modifier = Modifier.size(20.dp),
132+
tint = MaterialTheme.appColors.primary
133+
)
134+
Spacer(modifier = Modifier.width(8.dp))
135+
Text(
136+
text = stringResource(R.string.course_completion_view_all_content),
137+
style = MaterialTheme.appTypography.labelLarge,
138+
color = MaterialTheme.appColors.primary
139+
)
140+
}
141+
}
142+
}
143+
}
144+
145+
@Preview
146+
@Composable
147+
private fun CourseCompletionHomePagerCardContentPreview() {
148+
OpenEdXTheme {
149+
CourseCompletionHomePagerCardContent(
150+
uiState = CourseHomeUIState.CourseData(
151+
courseStructure = Mock.mockCourseStructure,
152+
courseProgress = null, // No course progress for preview
153+
next = Pair(Mock.mockChapterBlock, Mock.mockChapterBlock), // Mock next section
154+
downloadedState = mapOf(),
155+
resumeComponent = Mock.mockChapterBlock,
156+
resumeUnitTitle = "Resumed Unit",
157+
courseSubSections = mapOf(),
158+
subSectionsDownloadsCount = mapOf(),
159+
datesBannerInfo = CourseDatesBannerInfo(
160+
missedDeadlines = false,
161+
missedGatedContent = false,
162+
verifiedUpgradeLink = "",
163+
contentTypeGatingEnabled = false,
164+
hasEnded = false
165+
),
166+
useRelativeDates = true
167+
),
168+
onViewAllContentClick = {},
169+
onDownloadClick = {},
170+
onSubSectionClick = {},
171+
)
172+
}
173+
}

0 commit comments

Comments
 (0)