Skip to content

Commit f3ffa98

Browse files
Feat: Home tab[0092] (openedx#462)
* feat: course home pager and pager indicator with navigation * feat: course completion pager tab * feat: move HomeNavigationRow to bottom bar * feat: course home pages videos card * feat: course home pages assignment card * feat: course home pages grades card * feat: added empty states * feat: CaughtUpMessage * feat: A11y * feat: detekt fixes * feat: changes according demo feedback * feat: course home analytic * feat: CourseHomeViewModelTest * fix: detekt fix and changes according PR review * feat: performance improvements * feat: changes according PR feedback * feat: changes according PR feedback
1 parent 6e45ae1 commit f3ffa98

File tree

57 files changed

+4208
-621
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4208
-621
lines changed

app/schemas/org.openedx.app.room.AppDatabase/4.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"formatVersion": 1,
33
"database": {
44
"version": 4,
5-
"identityHash": "488bd2b78e977fef626afb28014c80f2",
5+
"identityHash": "7ea446decde04c9c16700cb3981703c2",
66
"entities": [
77
{
88
"tableName": "course_discovery_table",
@@ -1008,7 +1008,7 @@
10081008
},
10091009
{
10101010
"tableName": "video_progress_table",
1011-
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`block_id` TEXT NOT NULL, `video_url` TEXT NOT NULL, `video_time` INTEGER NOT NULL, `duration` INTEGER NOT NULL, PRIMARY KEY(`block_id`))",
1011+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`block_id` TEXT NOT NULL, `video_url` TEXT NOT NULL, `video_time` INTEGER, `duration` INTEGER, PRIMARY KEY(`block_id`))",
10121012
"fields": [
10131013
{
10141014
"fieldPath": "blockId",
@@ -1026,13 +1026,13 @@
10261026
"fieldPath": "videoTime",
10271027
"columnName": "video_time",
10281028
"affinity": "INTEGER",
1029-
"notNull": true
1029+
"notNull": false
10301030
},
10311031
{
10321032
"fieldPath": "duration",
10331033
"columnName": "duration",
10341034
"affinity": "INTEGER",
1035-
"notNull": true
1035+
"notNull": false
10361036
}
10371037
],
10381038
"primaryKey": {
@@ -1230,7 +1230,7 @@
12301230
"views": [],
12311231
"setupQueries": [
12321232
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
1233-
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '488bd2b78e977fef626afb28014c80f2')"
1233+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7ea446decde04c9c16700cb3981703c2')"
12341234
]
12351235
}
12361236
}

app/src/main/java/org/openedx/app/AppActivity.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package org.openedx.app
22

33
import android.content.Intent
44
import android.content.res.Configuration
5+
import android.graphics.Color
56
import android.net.Uri
7+
import android.os.Build
68
import android.os.Bundle
79
import android.view.View
810
import android.view.WindowManager
@@ -158,8 +160,12 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
158160
WindowCompat.setDecorFitsSystemWindows(this, false)
159161
val insetsController = WindowInsetsControllerCompat(this, binding.root)
160162
insetsController.isAppearanceLightStatusBars = !isUsingNightModeResources()
161-
insetsController.systemBarsBehavior =
162-
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
163+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
164+
insetsController.systemBarsBehavior =
165+
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
166+
} else {
167+
window.statusBarColor = Color.TRANSPARENT
168+
}
163169
}
164170
}
165171

app/src/main/java/org/openedx/app/AppRouter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import org.openedx.auth.presentation.signin.SignInFragment
1111
import org.openedx.auth.presentation.signup.SignUpFragment
1212
import org.openedx.core.CalendarRouter
1313
import org.openedx.core.FragmentViewType
14-
import org.openedx.core.presentation.course.CourseViewMode
1514
import org.openedx.core.presentation.global.appupgrade.AppUpgradeRouter
1615
import org.openedx.core.presentation.global.appupgrade.UpgradeRequiredFragment
1716
import org.openedx.core.presentation.global.webview.WebContentFragment
@@ -24,6 +23,7 @@ import org.openedx.course.presentation.handouts.HandoutsType
2423
import org.openedx.course.presentation.handouts.HandoutsWebViewFragment
2524
import org.openedx.course.presentation.section.CourseSectionFragment
2625
import org.openedx.course.presentation.unit.container.CourseUnitContainerFragment
26+
import org.openedx.course.presentation.unit.container.CourseViewMode
2727
import org.openedx.course.presentation.unit.video.VideoFullScreenFragment
2828
import org.openedx.course.presentation.unit.video.YoutubeVideoFullScreenFragment
2929
import org.openedx.course.settings.download.DownloadQueueFragment

app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import org.openedx.auth.presentation.signin.SignInFragment
1111
import org.openedx.core.FragmentViewType
1212
import org.openedx.core.config.Config
1313
import org.openedx.core.data.storage.CorePreferences
14-
import org.openedx.core.presentation.course.CourseViewMode
1514
import org.openedx.course.domain.interactor.CourseInteractor
1615
import org.openedx.course.presentation.handouts.HandoutsType
16+
import org.openedx.course.presentation.unit.container.CourseViewMode
1717
import org.openedx.discovery.domain.interactor.DiscoveryInteractor
1818
import org.openedx.discovery.domain.model.Course
1919
import org.openedx.discovery.presentation.catalog.WebViewLink

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.openedx.course.presentation.container.CourseContainerViewModel
2323
import org.openedx.course.presentation.contenttab.ContentTabViewModel
2424
import org.openedx.course.presentation.dates.CourseDatesViewModel
2525
import org.openedx.course.presentation.handouts.HandoutsViewModel
26+
import org.openedx.course.presentation.home.CourseHomeViewModel
2627
import org.openedx.course.presentation.offline.CourseOfflineViewModel
2728
import org.openedx.course.presentation.outline.CourseContentAllViewModel
2829
import org.openedx.course.presentation.progress.CourseProgressViewModel
@@ -309,6 +310,27 @@ val screenModule = module {
309310
get(),
310311
)
311312
}
313+
viewModel { (courseId: String, courseTitle: String) ->
314+
CourseHomeViewModel(
315+
courseId,
316+
courseTitle,
317+
get(),
318+
get(),
319+
get(),
320+
get(),
321+
get(),
322+
get(),
323+
get(),
324+
get(),
325+
get(),
326+
get(),
327+
get(),
328+
get(),
329+
get(),
330+
get(),
331+
get()
332+
)
333+
}
312334
viewModel { (courseId: String) ->
313335
CourseSectionViewModel(
314336
courseId,
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package org.openedx.core
2+
3+
import org.openedx.core.data.model.room.VideoProgressEntity
4+
import org.openedx.core.domain.model.AssignmentProgress
5+
import org.openedx.core.domain.model.Block
6+
import org.openedx.core.domain.model.BlockCounts
7+
import org.openedx.core.domain.model.CourseComponentStatus
8+
import org.openedx.core.domain.model.CourseDatesBannerInfo
9+
import org.openedx.core.domain.model.CourseDatesResult
10+
import org.openedx.core.domain.model.CourseProgress
11+
import org.openedx.core.domain.model.CourseStructure
12+
import org.openedx.core.domain.model.CoursewareAccess
13+
import org.openedx.core.domain.model.EncodedVideos
14+
import org.openedx.core.domain.model.OfflineDownload
15+
import org.openedx.core.domain.model.Progress
16+
import org.openedx.core.domain.model.ResetCourseDates
17+
import org.openedx.core.domain.model.StudentViewData
18+
import org.openedx.core.domain.model.VideoInfo
19+
import org.openedx.core.module.db.DownloadModel
20+
import org.openedx.core.module.db.DownloadedState
21+
import org.openedx.core.module.db.FileType
22+
import java.util.Date
23+
24+
object Mock {
25+
private val mockAssignmentProgress = AssignmentProgress(
26+
assignmentType = "Home",
27+
numPointsEarned = 1f,
28+
numPointsPossible = 3f,
29+
shortLabel = "HM1"
30+
)
31+
val mockChapterBlock = Block(
32+
id = "id",
33+
blockId = "blockId",
34+
lmsWebUrl = "lmsWebUrl",
35+
legacyWebUrl = "legacyWebUrl",
36+
studentViewUrl = "studentViewUrl",
37+
type = BlockType.CHAPTER,
38+
displayName = "Chapter",
39+
graded = false,
40+
studentViewData = null,
41+
studentViewMultiDevice = false,
42+
blockCounts = BlockCounts(1),
43+
descendants = emptyList(),
44+
descendantsType = BlockType.CHAPTER,
45+
completion = 0.0,
46+
containsGatedContent = false,
47+
assignmentProgress = mockAssignmentProgress,
48+
due = Date(),
49+
offlineDownload = null
50+
)
51+
private val mockSequentialBlock = Block(
52+
id = "id",
53+
blockId = "blockId",
54+
lmsWebUrl = "lmsWebUrl",
55+
legacyWebUrl = "legacyWebUrl",
56+
studentViewUrl = "studentViewUrl",
57+
type = BlockType.SEQUENTIAL,
58+
displayName = "Sequential",
59+
graded = false,
60+
studentViewData = null,
61+
studentViewMultiDevice = false,
62+
blockCounts = BlockCounts(1),
63+
descendants = emptyList(),
64+
descendantsType = BlockType.CHAPTER,
65+
completion = 0.0,
66+
containsGatedContent = false,
67+
assignmentProgress = mockAssignmentProgress,
68+
due = Date(),
69+
offlineDownload = OfflineDownload("fileUrl", "", 1),
70+
)
71+
72+
val mockCourseStructure = CourseStructure(
73+
root = "",
74+
blockData = listOf(mockSequentialBlock, mockSequentialBlock),
75+
id = "id",
76+
name = "Course name",
77+
number = "",
78+
org = "Org",
79+
start = Date(),
80+
startDisplay = "",
81+
startType = "",
82+
end = Date(),
83+
coursewareAccess = CoursewareAccess(
84+
true,
85+
"",
86+
"",
87+
"",
88+
"",
89+
""
90+
),
91+
media = null,
92+
certificate = null,
93+
isSelfPaced = false,
94+
progress = Progress(1, 3),
95+
)
96+
97+
val mockCourseComponentStatus = CourseComponentStatus(
98+
lastVisitedBlockId = "video1"
99+
)
100+
101+
val mockCourseDatesBannerInfo = CourseDatesBannerInfo(
102+
missedDeadlines = false,
103+
missedGatedContent = false,
104+
contentTypeGatingEnabled = false,
105+
verifiedUpgradeLink = "",
106+
hasEnded = false
107+
)
108+
109+
val mockCourseDatesResult = CourseDatesResult(
110+
datesSection = linkedMapOf(),
111+
courseBanner = mockCourseDatesBannerInfo
112+
)
113+
114+
val mockCourseProgress = CourseProgress(
115+
verifiedMode = "audit",
116+
accessExpiration = "",
117+
certificateData = null,
118+
completionSummary = null,
119+
courseGrade = null,
120+
creditCourseRequirements = "",
121+
end = "",
122+
enrollmentMode = "audit",
123+
gradingPolicy = null,
124+
hasScheduledContent = false,
125+
sectionScores = emptyList(),
126+
studioUrl = "",
127+
username = "testuser",
128+
userHasPassingGrade = false,
129+
verificationData = null,
130+
disableProgressGraph = false
131+
)
132+
133+
val mockVideoProgress = VideoProgressEntity(
134+
blockId = "video1",
135+
videoUrl = "test-video-url",
136+
videoTime = 1000L,
137+
duration = 5000L
138+
)
139+
140+
val mockResetCourseDates = ResetCourseDates(
141+
message = "Dates reset successfully",
142+
body = "Your course dates have been reset",
143+
header = "Success",
144+
link = "",
145+
linkText = ""
146+
)
147+
148+
val mockDownloadModel = DownloadModel(
149+
id = "video1",
150+
title = "Video 1",
151+
courseId = "test-course-id",
152+
size = 1000L,
153+
path = "/test/path/video1",
154+
url = "test-url",
155+
type = FileType.VIDEO,
156+
downloadedState = DownloadedState.NOT_DOWNLOADED,
157+
lastModified = null
158+
)
159+
160+
val mockVideoBlock = Block(
161+
id = "video1",
162+
blockId = "video1",
163+
lmsWebUrl = "lmsWebUrl",
164+
legacyWebUrl = "legacyWebUrl",
165+
studentViewUrl = "studentViewUrl",
166+
type = BlockType.VIDEO,
167+
displayName = "Video 1",
168+
graded = false,
169+
studentViewData = StudentViewData(
170+
onlyOnWeb = false,
171+
duration = "",
172+
transcripts = null,
173+
encodedVideos = EncodedVideos(
174+
youtube = null,
175+
hls = null,
176+
fallback = null,
177+
desktopMp4 = null,
178+
mobileHigh = null,
179+
mobileLow = VideoInfo(
180+
url = "test-url",
181+
fileSize = 1000L
182+
)
183+
),
184+
topicId = ""
185+
),
186+
studentViewMultiDevice = false,
187+
blockCounts = BlockCounts(0),
188+
descendants = emptyList(),
189+
descendantsType = BlockType.VIDEO,
190+
completion = 0.0,
191+
containsGatedContent = false,
192+
assignmentProgress = null,
193+
due = null,
194+
offlineDownload = null,
195+
)
196+
197+
val mockSequentialBlockForDownload = Block(
198+
id = "sequential1",
199+
blockId = "sequential1",
200+
lmsWebUrl = "lmsWebUrl",
201+
legacyWebUrl = "legacyWebUrl",
202+
studentViewUrl = "studentViewUrl",
203+
type = BlockType.SEQUENTIAL,
204+
displayName = "Sequential 1",
205+
graded = false,
206+
studentViewData = null,
207+
studentViewMultiDevice = false,
208+
blockCounts = BlockCounts(0),
209+
descendants = listOf("vertical1"),
210+
descendantsType = BlockType.VERTICAL,
211+
completion = 0.0,
212+
containsGatedContent = false,
213+
assignmentProgress = null,
214+
due = null,
215+
offlineDownload = null,
216+
)
217+
218+
val mockVerticalBlock = Block(
219+
id = "vertical1",
220+
blockId = "vertical1",
221+
lmsWebUrl = "lmsWebUrl",
222+
legacyWebUrl = "legacyWebUrl",
223+
studentViewUrl = "studentViewUrl",
224+
type = BlockType.VERTICAL,
225+
displayName = "Vertical 1",
226+
graded = false,
227+
studentViewData = null,
228+
studentViewMultiDevice = false,
229+
blockCounts = BlockCounts(0),
230+
descendants = listOf("video1"),
231+
descendantsType = BlockType.VIDEO,
232+
completion = 0.0,
233+
containsGatedContent = false,
234+
assignmentProgress = null,
235+
due = null,
236+
offlineDownload = null,
237+
)
238+
239+
val mockCourseStructureForDownload = CourseStructure(
240+
root = "sequential1",
241+
blockData = listOf(mockSequentialBlockForDownload, mockVerticalBlock, mockVideoBlock),
242+
id = "test-course-id",
243+
name = "Test Course",
244+
number = "CS101",
245+
org = "TestOrg",
246+
start = Date(),
247+
startDisplay = "2024-01-01",
248+
startType = "timestamped",
249+
end = Date(),
250+
coursewareAccess = CoursewareAccess(
251+
true,
252+
"",
253+
"",
254+
"",
255+
"",
256+
""
257+
),
258+
media = null,
259+
certificate = null,
260+
isSelfPaced = false,
261+
progress = null
262+
)
263+
}

0 commit comments

Comments
 (0)