Skip to content

Commit 8625632

Browse files
feat: course home pages videos card
1 parent 7d6610f commit 8625632

File tree

8 files changed

+364
-112
lines changed

8 files changed

+364
-112
lines changed

app/src/main/java/org/openedx/app/di/ScreenModule.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ val screenModule = module {
328328
get(),
329329
get(),
330330
get(),
331+
get()
331332
)
332333
}
333334
viewModel { (courseId: String) ->

course/src/main/java/org/openedx/course/presentation/home/CourseCompletionHomePagerCardContent.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,10 @@ private fun CourseCompletionHomePagerCardContentPreview() {
163163
contentTypeGatingEnabled = false,
164164
hasEnded = false
165165
),
166-
useRelativeDates = true
166+
useRelativeDates = true,
167+
courseVideos = mapOf(),
168+
videoPreview = null,
169+
videoProgress = 0f
167170
),
168171
onViewAllContentClick = {},
169172
onDownloadClick = {},

course/src/main/java/org/openedx/course/presentation/home/CourseHomeScreen.kt

Lines changed: 127 additions & 106 deletions
Large diffs are not rendered by default.

course/src/main/java/org/openedx/course/presentation/home/CourseHomeUIState.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.openedx.core.domain.model.CourseDatesBannerInfo
55
import org.openedx.core.domain.model.CourseProgress
66
import org.openedx.core.domain.model.CourseStructure
77
import org.openedx.core.module.db.DownloadedState
8+
import org.openedx.core.utils.VideoPreview
89

910
sealed class CourseHomeUIState {
1011
data class CourseData(
@@ -18,6 +19,9 @@ sealed class CourseHomeUIState {
1819
val subSectionsDownloadsCount: Map<String, Int>,
1920
val datesBannerInfo: CourseDatesBannerInfo,
2021
val useRelativeDates: Boolean,
22+
val courseVideos: Map<String, List<Block>>,
23+
val videoPreview: VideoPreview?,
24+
val videoProgress: Float,
2125
) : CourseHomeUIState()
2226

2327
data object Error : CourseHomeUIState()

course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.openedx.course.presentation.home
22

3+
import android.content.Context
34
import androidx.fragment.app.FragmentManager
45
import androidx.lifecycle.viewModelScope
56
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -51,6 +52,7 @@ import org.openedx.course.R as courseR
5152
class CourseHomeViewModel(
5253
val courseId: String,
5354
private val courseTitle: String,
55+
private val context: Context,
5456
private val config: Config,
5557
private val interactor: CourseInteractor,
5658
private val resourceManager: ResourceManager,
@@ -94,6 +96,7 @@ class CourseHomeViewModel(
9496
private val courseSubSections = mutableMapOf<String, MutableList<Block>>()
9597
private val subSectionsDownloadsCount = mutableMapOf<String, Int>()
9698
val courseSubSectionUnit = mutableMapOf<String, Block?>()
99+
private val courseVideos = mutableMapOf<String, MutableList<Block>>()
97100

98101
init {
99102
viewModelScope.launch {
@@ -130,7 +133,10 @@ class CourseHomeViewModel(
130133
datesBannerInfo = state.datesBannerInfo,
131134
useRelativeDates = preferencesManager.isRelativeDatesEnabled,
132135
next = state.next,
133-
courseProgress = state.courseProgress
136+
courseProgress = state.courseProgress,
137+
courseVideos = state.courseVideos,
138+
videoPreview = state.videoPreview,
139+
videoProgress = state.videoProgress
134140
)
135141
}
136142
}
@@ -201,10 +207,32 @@ class CourseHomeViewModel(
201207
setBlocks(blocks)
202208
courseSubSections.clear()
203209
courseSubSectionUnit.clear()
210+
courseVideos.clear()
204211
val sortedStructure = courseStructure.copy(blockData = sortBlocks(blocks))
205212
initDownloadModelsStatus()
206213
val nextSection = findFirstChapterWithIncompleteDescendants(blocks)
207214

215+
// Get video data
216+
val allVideos = courseVideos.values.flatten()
217+
val firstIncompleteVideo = allVideos.find { !it.isCompleted() }
218+
val videoPreview = firstIncompleteVideo?.getVideoPreview(
219+
context,
220+
networkConnection.isOnline(),
221+
null
222+
)
223+
val videoProgress = if (firstIncompleteVideo != null) {
224+
try {
225+
val videoProgressEntity = interactor.getVideoProgress(firstIncompleteVideo.id)
226+
val progress =
227+
videoProgressEntity.videoTime.toFloat() / videoProgressEntity.duration.toFloat()
228+
progress.coerceIn(0f, 1f)
229+
} catch (e: Exception) {
230+
0f
231+
}
232+
} else {
233+
0f
234+
}
235+
208236
_uiState.value = CourseHomeUIState.CourseData(
209237
courseStructure = sortedStructure,
210238
next = nextSection,
@@ -215,7 +243,10 @@ class CourseHomeViewModel(
215243
subSectionsDownloadsCount = subSectionsDownloadsCount,
216244
datesBannerInfo = datesBannerInfo,
217245
useRelativeDates = preferencesManager.isRelativeDatesEnabled,
218-
courseProgress = courseProgress
246+
courseProgress = courseProgress,
247+
courseVideos = courseVideos,
248+
videoPreview = videoPreview,
249+
videoProgress = videoProgress
219250
)
220251
}
221252

@@ -250,13 +281,30 @@ class CourseHomeViewModel(
250281
subSectionsDownloadsCount[sequentialBlock.id] =
251282
sequentialBlock.getDownloadsCount(blocks)
252283
addDownloadableChildrenForSequentialBlock(sequentialBlock)
284+
285+
// Add video processing logic
286+
val verticalBlocks = blocks.filter { block ->
287+
block.id in sequentialBlock.descendants
288+
}
289+
val videoBlocks = blocks.filter { block ->
290+
verticalBlocks.any { vertical -> block.id in vertical.descendants } && block.type == BlockType.VIDEO
291+
}
292+
addToVideos(block, videoBlocks)
253293
}
254294
}
255295

256296
private fun addSequentialBlockToSubSections(block: Block, sequentialBlock: Block) {
257297
courseSubSections.getOrPut(block.id) { mutableListOf() }.add(sequentialBlock)
258298
}
259299

300+
private fun addToVideos(chapterBlock: Block, videoBlocks: List<Block>) {
301+
courseVideos.getOrPut(chapterBlock.id) { mutableListOf() }.addAll(videoBlocks)
302+
}
303+
304+
fun getBlockParent(blockId: String): Block? {
305+
return allBlocks.values.find { blockId in it.descendants }
306+
}
307+
260308
private fun getResumeBlock(
261309
blocks: List<Block>,
262310
continueBlockId: String,
@@ -485,4 +533,21 @@ class CourseHomeViewModel(
485533
}
486534
}
487535
}
536+
537+
fun logVideoClick(blockId: String) {
538+
val currentState = uiState.value
539+
if (currentState is CourseHomeUIState.CourseData) {
540+
analytics.logEvent(
541+
CourseAnalyticsEvent.COURSE_CONTENT_VIDEO_CLICK.eventName,
542+
buildMap {
543+
put(
544+
CourseAnalyticsKey.NAME.key,
545+
CourseAnalyticsEvent.COURSE_CONTENT_VIDEO_CLICK.biValue
546+
)
547+
put(CourseAnalyticsKey.COURSE_ID.key, courseId)
548+
put(CourseAnalyticsKey.BLOCK_ID.key, blockId)
549+
}
550+
)
551+
}
552+
}
488553
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package org.openedx.course.presentation.home
2+
3+
import androidx.compose.foundation.layout.Box
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.foundation.shape.CircleShape
13+
import androidx.compose.material.Icon
14+
import androidx.compose.material.LinearProgressIndicator
15+
import androidx.compose.material.MaterialTheme
16+
import androidx.compose.material.Text
17+
import androidx.compose.material.TextButton
18+
import androidx.compose.material.icons.Icons
19+
import androidx.compose.material.icons.automirrored.filled.List
20+
import androidx.compose.material.icons.filled.Videocam
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.clip
25+
import androidx.compose.ui.res.stringResource
26+
import androidx.compose.ui.text.font.FontWeight
27+
import androidx.compose.ui.unit.dp
28+
import org.openedx.core.domain.model.Block
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.ui.CourseVideoItem
33+
34+
@Composable
35+
fun VideosHomePagerCardContent(
36+
uiState: CourseHomeUIState.CourseData,
37+
onVideoClick: (Block) -> Unit,
38+
onViewAllVideosClick: () -> Unit
39+
) {
40+
val allVideos = uiState.courseVideos.values.flatten()
41+
val completedVideos = allVideos.count { it.isCompleted() }
42+
val totalVideos = allVideos.size
43+
val firstIncompleteVideo = allVideos.find { !it.isCompleted() }
44+
45+
Column(
46+
modifier = Modifier
47+
.fillMaxWidth()
48+
.padding(16.dp)
49+
) {
50+
// Header with progress
51+
Text(
52+
text = stringResource(R.string.course_container_content_tab_video),
53+
style = MaterialTheme.appTypography.titleLarge,
54+
color = MaterialTheme.appColors.textPrimary,
55+
fontWeight = FontWeight.SemiBold
56+
)
57+
Spacer(modifier = Modifier.height(12.dp))
58+
Row(
59+
modifier = Modifier.fillMaxWidth(),
60+
verticalAlignment = Alignment.CenterVertically
61+
) {
62+
Icon(
63+
imageVector = Icons.Filled.Videocam,
64+
contentDescription = null,
65+
tint = MaterialTheme.appColors.textPrimary,
66+
modifier = Modifier.size(32.dp)
67+
)
68+
Spacer(modifier = Modifier.width(8.dp))
69+
Text(
70+
text = "$completedVideos/$totalVideos",
71+
style = MaterialTheme.appTypography.displaySmall,
72+
color = MaterialTheme.appColors.textPrimary,
73+
fontWeight = FontWeight.Bold
74+
)
75+
Spacer(modifier = Modifier.width(8.dp))
76+
Text(
77+
text = stringResource(R.string.course_videos_completed),
78+
style = MaterialTheme.appTypography.labelLarge,
79+
color = MaterialTheme.appColors.textPrimaryVariant,
80+
fontWeight = FontWeight.Medium
81+
)
82+
}
83+
84+
Spacer(modifier = Modifier.height(8.dp))
85+
86+
// Progress bar
87+
LinearProgressIndicator(
88+
modifier = Modifier
89+
.fillMaxWidth()
90+
.height(4.dp)
91+
.clip(CircleShape),
92+
progress = if (totalVideos > 0) completedVideos.toFloat() / totalVideos else 0f,
93+
color = MaterialTheme.appColors.progressBarColor,
94+
backgroundColor = MaterialTheme.appColors.progressBarBackgroundColor
95+
)
96+
97+
Spacer(modifier = Modifier.height(20.dp))
98+
99+
// Continue Watching section
100+
if (firstIncompleteVideo != null) {
101+
Text(
102+
text = stringResource(R.string.course_continue_watching),
103+
style = MaterialTheme.appTypography.titleMedium,
104+
color = MaterialTheme.appColors.textPrimary,
105+
fontWeight = FontWeight.SemiBold
106+
)
107+
108+
Spacer(modifier = Modifier.height(8.dp))
109+
110+
// Video card using CourseVideoItem
111+
Box(
112+
modifier = Modifier.fillMaxWidth(),
113+
contentAlignment = Alignment.Center
114+
) {
115+
CourseVideoItem(
116+
modifier = Modifier
117+
.fillMaxWidth()
118+
.height(180.dp),
119+
videoBlock = firstIncompleteVideo,
120+
preview = uiState.videoPreview,
121+
progress = uiState.videoProgress,
122+
onClick = {
123+
onVideoClick(firstIncompleteVideo)
124+
}
125+
)
126+
}
127+
}
128+
129+
Spacer(modifier = Modifier.height(8.dp))
130+
131+
// View All Videos button
132+
TextButton(
133+
onClick = onViewAllVideosClick,
134+
modifier = Modifier.fillMaxWidth()
135+
) {
136+
Icon(
137+
imageVector = Icons.AutoMirrored.Filled.List,
138+
contentDescription = null,
139+
tint = MaterialTheme.appColors.primary,
140+
modifier = Modifier.size(20.dp)
141+
)
142+
Spacer(modifier = Modifier.width(8.dp))
143+
Text(
144+
text = stringResource(R.string.course_view_all_videos),
145+
style = MaterialTheme.appTypography.labelLarge,
146+
color = MaterialTheme.appColors.primary
147+
)
148+
}
149+
}
150+
}

course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,9 @@ fun CourseVideoSection(
664664
) {
665665
items(videoBlocks) { block ->
666666
CourseVideoItem(
667+
modifier = Modifier
668+
.width(192.dp)
669+
.height(108.dp),
667670
videoBlock = block,
668671
preview = preview[block.id],
669672
progress = progress[block.id] ?: 0f,
@@ -679,15 +682,14 @@ fun CourseVideoSection(
679682

680683
@Composable
681684
fun CourseVideoItem(
685+
modifier: Modifier = Modifier,
682686
videoBlock: Block,
683687
preview: VideoPreview?,
684688
progress: Float,
685689
onClick: () -> Unit
686690
) {
687691
Box(
688-
modifier = Modifier
689-
.width(192.dp)
690-
.height(108.dp)
692+
modifier = modifier
691693
.clip(MaterialTheme.appShapes.videoPreviewShape)
692694
.let {
693695
if (videoBlock.isCompleted()) {

course/src/main/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,10 @@
8989
<string name="course_of_grade">%1$s %% of Grade</string>
9090
<string name="course_review_grading_policy">Review Course Grading Policy</string>
9191
<string name="course_return_to_course_home">Return to Course Home</string>
92+
93+
<!-- Videos UI -->
94+
<string name="course_continue_watching">Continue Watching</string>
95+
<string name="course_view_all_videos">View All Videos</string>
96+
<string name="course_video_time_remaining">%1$s left</string>
97+
<string name="course_videos_completed">Videos\ncompleted</string>
9298
</resources>

0 commit comments

Comments
 (0)