diff --git a/src/core/components/progress-bar/progress-bar.scss b/src/core/components/progress-bar/progress-bar.scss index 7937c4595..9d9587029 100644 --- a/src/core/components/progress-bar/progress-bar.scss +++ b/src/core/components/progress-bar/progress-bar.scss @@ -47,6 +47,7 @@ &[value]::-webkit-progress-value { background-color: var(--progressbar-color); border-radius: var(--height); + transition: width 500ms ease-in-out; } } diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index 950a509c3..af1ef4e35 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -21,10 +21,13 @@ import { CoreCourses, CoreCourseAnyCourseData } from '@features/courses/services import { CoreCourse, CoreCourseCompletionActivityStatus, + CoreCourseModuleCompletionStatus, + CoreCourseProvider, } from '@features/course/services/course'; import { CoreCourseHelper, CoreCourseModuleCompletionData, + CoreCourseModuleData, CoreCourseSection, } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; @@ -38,6 +41,7 @@ import { import { CoreNavigator } from '@services/navigator'; import { CoreRefreshContext, CORE_REFRESH_CONTEXT } from '@/core/utils/refresh-context'; import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; +import { CoreSites } from '@services/sites'; /** * Page that displays the contents of a course. @@ -315,20 +319,49 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon * @returns Promise resolved when done. */ async onCompletionChange(completionData: CoreCourseModuleCompletionData): Promise { - const shouldReload = completionData.valueused === undefined || completionData.valueused; - - if (!shouldReload) { - // Invalidate the completion. - await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.course.id)); - - this.debouncedUpdateCachedCompletion?.(); - + if (completionData.courseId != this.course?.id) { return; } - await CoreUtils.ignoreErrors(this.invalidateData()); + const siteId = CoreSites.getCurrentSiteId(); + const shouldReload = completionData.valueused === true; - await this.showLoadingAndRefresh(true, false); + if (!shouldReload) { + + if (!this.course || !('progress' in this.course) || typeof this.course.progress != 'number') { + return; + } + + if (this.sections) { + // If the completion value is not used, the page won't be reloaded, so update the progress bar. + const completionModules = ( []) + .concat(...this.sections.map((section) => section.modules)) + .map((module) => module.completion && module.completion > 0 ? 1 : module.completion) + .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0); + + const moduleProgressPercent = 100 / (completionModules || 1); + // Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar. + if (completionData.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { + this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent); + } else { + this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent); + } + } + + await CoreUtils.ignoreErrors(this.invalidateData()); + this.debouncedUpdateCachedCompletion?.(); + } else { + await CoreUtils.ignoreErrors(this.invalidateData()); + await this.showLoadingAndRefresh(true, false); + } + + if (!('progress' in this.course) || this.course.progress === undefined || this.course.progress === null) { + return; + } + + CoreEvents.trigger(CoreCourseProvider.PROGRESS_UPDATED, { + courseId: this.course.id, progress: this.course.progress, + }, siteId); } /** diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index e5666f575..fe16b6e3b 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -20,7 +20,7 @@ import { CoreCourseFormatDelegate } from '../../services/format-delegate'; import { CoreCourseOptionsDelegate } from '../../services/course-options-delegate'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseWSSection } from '@features/course/services/course'; +import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course'; import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; @@ -29,6 +29,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '@features/courses/services/courses-helper'; import { CoreColors } from '@singletons/colors'; import { CorePath } from '@singletons/path'; +import { CoreSites } from '@services/sites'; /** * Page that displays the list of courses the user is enrolled in. @@ -54,7 +55,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { protected currentPagePath = ''; protected fullScreenObserver: CoreEventObserver; protected selectTabObserver: CoreEventObserver; - protected completionObserver: CoreEventObserver; + protected progressObserver: CoreEventObserver; protected sections: CoreCourseWSSection[] = []; // List of course sections. protected firstTabName?: string; protected module?: CoreCourseModuleData; @@ -89,33 +90,16 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { } }); - // The completion of any of the modules have changed. - this.completionObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => { - if (data.completion.courseId != this.course?.id) { + const siteId = CoreSites.getCurrentSiteId(); + + this.progressObserver = CoreEvents.on(CoreCourseProvider.PROGRESS_UPDATED, (data) => { + if (!this.course || this.course.id !== data.courseId || !('progress' in this.course)) { return; } - if (data.completion.valueused !== false || !this.course || !('progress' in this.course) || - typeof this.course.progress != 'number') { - return; - } - - // If the completion value is not used, the page won't be reloaded, so update the progress bar. - const completionModules = ( []) - .concat(...this.sections.map((section) => section.modules)) - .map((module) => module.completion && module.completion > 0 ? 1 : module.completion) - .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0); - - const moduleProgressPercent = 100 / (completionModules || 1); - // Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar. - if (data.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { - this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent); - } else { - this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent); - } - + this.course.progress = data.progress; this.updateProgress(); - }); + }, siteId); this.fullScreenObserver = CoreEvents.on(CoreEvents.FULL_SCREEN_CHANGED, (event: { enabled: boolean }) => { this.fullScreenEnabled = event.enabled; @@ -267,7 +251,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { CoreNavigator.decreaseRouteDepth(path.replace(/(\/deep)+/, '')); this.selectTabObserver?.off(); - this.completionObserver?.off(); + this.progressObserver?.off(); this.fullScreenObserver?.off(); } diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index f9122e912..6542edd7f 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -64,6 +64,8 @@ import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-si const ROOT_CACHE_KEY = 'mmCourse:'; +export type CoreCourseProgressUpdated = { progress: number; courseId: number }; + declare module '@singletons/events' { /** @@ -73,6 +75,7 @@ declare module '@singletons/events' { */ export interface CoreEventsData { [CoreCourseSyncProvider.AUTO_SYNCED]: CoreCourseAutoSyncData; + [CoreCourseProvider.PROGRESS_UPDATED]: CoreCourseProgressUpdated; } } @@ -121,6 +124,7 @@ export class CoreCourseProvider { static readonly ALL_SECTIONS_ID = -2; static readonly STEALTH_MODULES_SECTION_ID = -1; static readonly ALL_COURSES_CLEARED = -1; + static readonly PROGRESS_UPDATED = 'progress_updated'; /** * @deprecated since 4.4 Not used anymore. Use CoreCourseAccessDataType instead. diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts index 50dfd1ef1..6c494ecd4 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.ts +++ b/src/core/features/courses/components/course-list-item/course-list-item.ts @@ -65,9 +65,19 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On protected courseStatusObserver?: CoreEventObserver; protected element: HTMLElement; + protected progressObserver: CoreEventObserver; constructor(element: ElementRef) { this.element = element.nativeElement; + const siteId = CoreSites.getCurrentSiteId(); + this.progressObserver = CoreEvents.on(CoreCourseProvider.PROGRESS_UPDATED, (data) => { + if (!this.course || this.course.id !== data.courseId || !('progress' in this.course)) { + return; + } + + this.course.progress = data.progress; + this.progress = this.course.progress ?? undefined; + }, siteId); } /** @@ -387,6 +397,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On ngOnDestroy(): void { this.isDestroyed = true; this.courseStatusObserver?.off(); + this.progressObserver.off(); } }