forked from CIT/Vmeda.Online
		
	MOBILE-4660 course: Adapt course downloads to new data structure
This commit is contained in:
		
							parent
							
								
									d384752113
								
							
						
					
					
						commit
						d1856f5fff
					
				@ -70,7 +70,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
 | 
			
		||||
        let modFullNames: Record<string, string> = {};
 | 
			
		||||
        const brandedIcons: Record<string, boolean|undefined> = {};
 | 
			
		||||
 | 
			
		||||
        const modules = CoreCourseHelper.getSectionsModules(sections, {
 | 
			
		||||
        const modules = CoreCourse.getSectionsModules(sections, {
 | 
			
		||||
            ignoreSection: section => !CoreCourseHelper.canUserViewSection(section),
 | 
			
		||||
            ignoreModule: module => !CoreCourseHelper.canUserViewModule(module) || !CoreCourse.moduleHasView(module),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
export const ADDON_MOD_SUBSECTION_COMPONENT = 'mmaModSubsection';
 | 
			
		||||
@ -1,98 +0,0 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
 | 
			
		||||
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSSection } from '@features/course/services/course';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { ADDON_MOD_SUBSECTION_COMPONENT } from '../../constants';
 | 
			
		||||
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
 | 
			
		||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to prefetch subsections.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModSubsectionPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModSubsection';
 | 
			
		||||
    modName = 'subsection';
 | 
			
		||||
    component = ADDON_MOD_SUBSECTION_COMPONENT;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async performDownloadOrPrefetch(
 | 
			
		||||
        siteId: string,
 | 
			
		||||
        module: CoreCourseModuleData,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const section = await this.getSection(module, courseId, siteId);
 | 
			
		||||
        if (!section) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreCourseHelper.prefetchSections([section], courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreFileSizeSum> {
 | 
			
		||||
        const section = await this.getSection(module, courseId);
 | 
			
		||||
        if (!section) {
 | 
			
		||||
            return { size: 0, total: true };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getDownloadedSize(module: CoreCourseAnyModuleData, courseId: number): Promise<number> {
 | 
			
		||||
        const section = await this.getSection(module, courseId);
 | 
			
		||||
        if (!section) {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CoreCourseHelper.getModulesDownloadedSize(section.modules, courseId);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the section of a module.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @returns Promise resolved with the section if found.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getSection(
 | 
			
		||||
        module: CoreCourseAnyModuleData,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<CoreCourseWSSection | undefined> {
 | 
			
		||||
        siteId = siteId ?? CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const sections = await CoreCourse.getSections(courseId, false, true, undefined, siteId);
 | 
			
		||||
 | 
			
		||||
        return sections.find((section) =>
 | 
			
		||||
            section.component === 'mod_subsection' && section.itemid === module.instance);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
export const AddonModSubsectionPrefetchHandler = makeSingleton(AddonModSubsectionPrefetchHandlerService);
 | 
			
		||||
@ -15,8 +15,6 @@
 | 
			
		||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
 | 
			
		||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link';
 | 
			
		||||
import { AddonModSubsectionPrefetchHandler } from './services/handlers/prefetch';
 | 
			
		||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    providers: [
 | 
			
		||||
@ -25,7 +23,6 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu
 | 
			
		||||
            multi: true,
 | 
			
		||||
            useValue: () => {
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance);
 | 
			
		||||
                CoreCourseModulePrefetchDelegate.registerHandler(AddonModSubsectionPrefetchHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <ion-card class="wholecourse">
 | 
			
		||||
            <ion-card-header>
 | 
			
		||||
                <ion-card-title>
 | 
			
		||||
@ -57,7 +56,7 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<ng-template #sectionCard let-section="section">
 | 
			
		||||
    <ion-card class="section" *ngIf="section.modules.length > 0" [id]="'addons-course-storage-'+section.id">
 | 
			
		||||
    <ion-card class="section" *ngIf="section.contents.length > 0" [id]="'addons-course-storage-'+section.id">
 | 
			
		||||
        <ion-accordion [value]="section.id" toggleIconSlot="start">
 | 
			
		||||
            <ion-item [detail]="false" slot="header" class="card-header">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
@ -102,45 +101,46 @@
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-card-content slot="content">
 | 
			
		||||
                <ng-container *ngIf="section.expanded">
 | 
			
		||||
                    <ng-container *ngFor="let module of section.modules">
 | 
			
		||||
                        @if (module.subSection) {
 | 
			
		||||
                        <ng-container *ngTemplateOutlet="sectionCard; context: { section: module.subSection }" />
 | 
			
		||||
                    <ng-container *ngFor="let modOrSubsection of section.contents">
 | 
			
		||||
                        @if (!isModule(modOrSubsection)) {
 | 
			
		||||
                        <ng-container *ngTemplateOutlet="sectionCard; context: { section: modOrSubsection }" />
 | 
			
		||||
                        } @else {
 | 
			
		||||
                        <ion-item class="core-course-storage-activity"
 | 
			
		||||
                            *ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
 | 
			
		||||
                            <core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
 | 
			
		||||
                                [modname]="module.modname" [componentId]="module.instance" [fallbackTranslation]="module.modplural"
 | 
			
		||||
                                [isBranded]="module.branded" />
 | 
			
		||||
                            *ngIf="downloadEnabled || (!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0)">
 | 
			
		||||
                            <core-mod-icon slot="start" *ngIf="modOrSubsection.handlerData.icon"
 | 
			
		||||
                                [modicon]="modOrSubsection.handlerData.icon" [modname]="modOrSubsection.modname"
 | 
			
		||||
                                [componentId]="modOrSubsection.instance" [fallbackTranslation]="modOrSubsection.modplural"
 | 
			
		||||
                                [isBranded]="modOrSubsection.branded" />
 | 
			
		||||
                            <ion-label class="ion-text-wrap">
 | 
			
		||||
                                <p class="item-heading {{module.handlerData!.class}} addon-storagemanager-module-size">
 | 
			
		||||
                                    <core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
 | 
			
		||||
                                        [contextInstanceId]="module.id" [adaptImg]="false" />
 | 
			
		||||
                                <p class="item-heading {{modOrSubsection.handlerData!.class}} addon-storagemanager-module-size">
 | 
			
		||||
                                    <core-format-text [text]="modOrSubsection.handlerData.title" [courseId]="modOrSubsection.course"
 | 
			
		||||
                                        contextLevel="module" [contextInstanceId]="modOrSubsection.id" [adaptImg]="false" />
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <ion-badge [color]="module.downloadStatus === statusDownloaded ? 'success' : 'light'"
 | 
			
		||||
                                    *ngIf="!module.calculatingSize && module.totalSize > 0">
 | 
			
		||||
                                    <ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus === statusDownloaded"
 | 
			
		||||
                                        [attr.aria-label]="'core.downloaded' | translate" />{{ module.totalSize |
 | 
			
		||||
                                <ion-badge [color]="modOrSubsection.downloadStatus === statusDownloaded ? 'success' : 'light'"
 | 
			
		||||
                                    *ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0">
 | 
			
		||||
                                    <ion-icon name="fam-cloud-done" *ngIf="modOrSubsection.downloadStatus === statusDownloaded"
 | 
			
		||||
                                        [attr.aria-label]="'core.downloaded' | translate" />{{ modOrSubsection.totalSize |
 | 
			
		||||
                                    coreBytesToSize }}
 | 
			
		||||
                                </ion-badge>
 | 
			
		||||
                                <ion-badge color="light" *ngIf="module.calculatingSize ||
 | 
			
		||||
                            (section.isDownloading && module.downloadStatus === statusDownloaded)">
 | 
			
		||||
                                <ion-badge color="light" *ngIf="modOrSubsection.calculatingSize ||
 | 
			
		||||
                            (section.isDownloading && modOrSubsection.downloadStatus === statusDownloaded)">
 | 
			
		||||
                                    {{ 'core.calculating' | translate }}
 | 
			
		||||
                                </ion-badge>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
 | 
			
		||||
                            <div class="storage-buttons" slot="end">
 | 
			
		||||
                                <core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
 | 
			
		||||
                            module.downloadStatus !== statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
 | 
			
		||||
                                    [canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
 | 
			
		||||
                                    (action)="prefetchModule(module)"
 | 
			
		||||
                                <core-download-refresh *ngIf="downloadEnabled && modOrSubsection.handlerData?.showDownloadButton &&
 | 
			
		||||
                            modOrSubsection.downloadStatus !== statusDownloaded" [status]="modOrSubsection.downloadStatus" [enabled]="true"
 | 
			
		||||
                                    [canTrustDownload]="true" [loading]="modOrSubsection.spinner || modOrSubsection.handlerData.spinner"
 | 
			
		||||
                                    (action)="prefetchModule(modOrSubsection)"
 | 
			
		||||
                                    [statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
 | 
			
		||||
                                    [statusSubject]="module.name" />
 | 
			
		||||
                                <ion-button fill="clear" (click)="deleteForModule($event, module)"
 | 
			
		||||
                                    *ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
 | 
			
		||||
                                    [statusSubject]="modOrSubsection.name" />
 | 
			
		||||
                                <ion-button fill="clear" (click)="deleteForModule($event, modOrSubsection)"
 | 
			
		||||
                                    *ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0" color="danger">
 | 
			
		||||
                                    <ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="
 | 
			
		||||
                                        'addon.storagemanager.deletedatafrom' | translate: { name: module.name }" />
 | 
			
		||||
                                        'addon.storagemanager.deletedatafrom' | translate: { name: modOrSubsection.name }" />
 | 
			
		||||
                                </ion-button>
 | 
			
		||||
                                <p *ngIf="!downloadEnabled || !module.handlerData?.showDownloadButton" class="sr-only">
 | 
			
		||||
                                <p *ngIf="!downloadEnabled || !modOrSubsection.handlerData?.showDownloadButton" class="sr-only">
 | 
			
		||||
                                    {{ 'core.notdownloadable' | translate }}
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
@ -14,10 +14,11 @@
 | 
			
		||||
 | 
			
		||||
import { CoreConstants, DownloadStatus } from '@/core/constants';
 | 
			
		||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourse, CoreCourseProvider, sectionContentIsModule } from '@features/course/services/course';
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourseHelper,
 | 
			
		||||
    CoreCourseModuleData,
 | 
			
		||||
    CoreCourseSection,
 | 
			
		||||
    CoreCourseSectionWithStatus,
 | 
			
		||||
    CorePrefetchStatusInfo,
 | 
			
		||||
} from '@features/course/services/course-helper';
 | 
			
		||||
@ -31,9 +32,8 @@ import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreArray } from '@singletons/array';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
import { CoreEventObserver, CoreEvents, CoreEventSectionStatusChangedData } from '@singletons/events';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the amount of file storage used by each activity on the course, and allows
 | 
			
		||||
@ -66,6 +66,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    statusDownloaded = DownloadStatus.DOWNLOADED;
 | 
			
		||||
    isModule = sectionContentIsModule;
 | 
			
		||||
 | 
			
		||||
    protected siteUpdatedObserver?: CoreEventObserver;
 | 
			
		||||
    protected courseStatusObserver?: CoreEventObserver;
 | 
			
		||||
@ -116,30 +117,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
        const sections = (await CoreCourse.getSections(this.courseId, false, true))
 | 
			
		||||
            .filter((section) => !CoreCourseHelper.isSectionStealth(section));
 | 
			
		||||
 | 
			
		||||
        const sectionsToRender = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
 | 
			
		||||
            .map(section => ({
 | 
			
		||||
                ...section,
 | 
			
		||||
                totalSize: 0,
 | 
			
		||||
                calculatingSize: false,
 | 
			
		||||
                expanded: section.id === initialSectionId,
 | 
			
		||||
                modules: section.modules.map(module => ({
 | 
			
		||||
                    ...module,
 | 
			
		||||
                    totalSize: 0,
 | 
			
		||||
                    calculatingSize: false,
 | 
			
		||||
                })),
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
        const subSections = sectionsToRender.filter((section) => section.component === 'mod_subsection');
 | 
			
		||||
 | 
			
		||||
        this.sections = sectionsToRender.filter((section) => section.component !== 'mod_subsection');
 | 
			
		||||
        this.sections.forEach((section) => {
 | 
			
		||||
            section.modules.forEach((module) => {
 | 
			
		||||
                if (module.modname === 'subsection') {
 | 
			
		||||
                    module.subSection = subSections.find((section) =>
 | 
			
		||||
                        section.component === 'mod_subsection' && section.itemid === module.instance);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
 | 
			
		||||
            .map(section => this.formatSection(section));
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
 | 
			
		||||
@ -165,6 +144,33 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
        this.changeDetectorRef.markForCheck();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Format a section.
 | 
			
		||||
     *
 | 
			
		||||
     * @param section Section to format.
 | 
			
		||||
     * @param expanded Whether section should be expanded.
 | 
			
		||||
     * @returns Formatted section,
 | 
			
		||||
     */
 | 
			
		||||
    protected formatSection(section: CoreCourseSection, expanded = false): AddonStorageManagerCourseSection {
 | 
			
		||||
        return {
 | 
			
		||||
            ...section,
 | 
			
		||||
            totalSize: 0,
 | 
			
		||||
            calculatingSize: true,
 | 
			
		||||
            expanded: expanded,
 | 
			
		||||
            contents: section.contents.map(modOrSubsection => {
 | 
			
		||||
                if (sectionContentIsModule(modOrSubsection)) {
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...modOrSubsection,
 | 
			
		||||
                        totalSize: 0,
 | 
			
		||||
                        calculatingSize: false,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return this.formatSection(modOrSubsection, expanded);
 | 
			
		||||
            }),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Init course prefetch information.
 | 
			
		||||
     */
 | 
			
		||||
@ -221,12 +227,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Get the affected section.
 | 
			
		||||
                const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, data.sectionId);
 | 
			
		||||
                if (!sectionFinder?.section) {
 | 
			
		||||
                const { section } = CoreCourseHelper.findSection(this.sections, { id: data.sectionId });
 | 
			
		||||
                if (!section) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const section = sectionFinder.section;
 | 
			
		||||
                // @todo: Handle parents too? It seems the SECTION_STATUS_CHANGED event is never triggered.
 | 
			
		||||
 | 
			
		||||
                // Check if the affected section is being downloaded.
 | 
			
		||||
                // If so, we don't update section status because it'll already be updated when the download finishes.
 | 
			
		||||
@ -247,30 +253,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
 | 
			
		||||
            let moduleFound: AddonStorageManagerModule | undefined;
 | 
			
		||||
 | 
			
		||||
            this.sections.some((section) =>
 | 
			
		||||
                section.modules.some((module) => {
 | 
			
		||||
                    if (module.id === data.componentId &&
 | 
			
		||||
                        module.prefetchHandler &&
 | 
			
		||||
                        data.component === module.prefetchHandler?.component) {
 | 
			
		||||
                        moduleFound = module;
 | 
			
		||||
 | 
			
		||||
                        return true;
 | 
			
		||||
                    } else if (module.subSection) {
 | 
			
		||||
                        return module.subSection.modules.some((module) => {
 | 
			
		||||
                            if (module.id === data.componentId &&
 | 
			
		||||
                                module.prefetchHandler &&
 | 
			
		||||
                                data.component === module.prefetchHandler?.component) {
 | 
			
		||||
                                moduleFound = module;
 | 
			
		||||
 | 
			
		||||
                                return true;
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return false;
 | 
			
		||||
            }));
 | 
			
		||||
            const modules = CoreCourse.getSectionsModules(this.sections);
 | 
			
		||||
            const moduleFound = modules.find(module => module.id === data.componentId && module.prefetchHandler &&
 | 
			
		||||
                data.component === module.prefetchHandler?.component);
 | 
			
		||||
 | 
			
		||||
            if (!moduleFound) {
 | 
			
		||||
                return;
 | 
			
		||||
@ -278,13 +263,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            // Call determineModuleStatus to get the right status to display.
 | 
			
		||||
            const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(moduleFound, data.status);
 | 
			
		||||
            if (moduleFound.subSection) {
 | 
			
		||||
                const data: CoreEventSectionStatusChangedData = {
 | 
			
		||||
                    sectionId: moduleFound.subSection.id,
 | 
			
		||||
                    courseId: this.courseId,
 | 
			
		||||
                };
 | 
			
		||||
                CoreEvents.trigger(CoreEvents.SECTION_STATUS_CHANGED, data, CoreSites.getCurrentSiteId());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update the status.
 | 
			
		||||
            this.updateModuleStatus(moduleFound, status);
 | 
			
		||||
@ -307,22 +285,15 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param sections Modules.
 | 
			
		||||
     */
 | 
			
		||||
    protected async updateSizes(sections: AddonStorageManagerCourseSection[]): Promise<void> {
 | 
			
		||||
        sections = CoreArray.unique(sections, 'id');
 | 
			
		||||
 | 
			
		||||
        this.calculatingSize = true;
 | 
			
		||||
        sections.forEach((section) => {
 | 
			
		||||
        CoreCourseHelper.flattenSections(sections).forEach((section) => {
 | 
			
		||||
            section.calculatingSize = true;
 | 
			
		||||
            section.modules.map((module) => {
 | 
			
		||||
                if (module.subSection) {
 | 
			
		||||
                    module.subSection.calculatingSize = true;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.changeDetectorRef.markForCheck();
 | 
			
		||||
 | 
			
		||||
        // Update only affected module sections.
 | 
			
		||||
        const modules = this.getAllModulesList(sections);
 | 
			
		||||
        const modules = CoreCourse.getSectionsModules(sections);
 | 
			
		||||
        await Promise.all(modules.map(async (module) => {
 | 
			
		||||
            await this.calculateModuleSize(module);
 | 
			
		||||
        }));
 | 
			
		||||
@ -333,13 +304,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            this.changeDetectorRef.markForCheck();
 | 
			
		||||
 | 
			
		||||
            section.modules.forEach((module) => {
 | 
			
		||||
                if (module.subSection) {
 | 
			
		||||
                    updateSectionSize(module.subSection);
 | 
			
		||||
                    module.totalSize = module.subSection.totalSize;
 | 
			
		||||
            section.contents.forEach((modOrSubsection) => {
 | 
			
		||||
                if (!sectionContentIsModule(modOrSubsection)) {
 | 
			
		||||
                    updateSectionSize(modOrSubsection);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                section.totalSize += module.totalSize ?? 0;
 | 
			
		||||
                section.totalSize += modOrSubsection.totalSize ?? 0;
 | 
			
		||||
                this.changeDetectorRef.markForCheck();
 | 
			
		||||
            });
 | 
			
		||||
            section.calculatingSize = false;
 | 
			
		||||
@ -390,7 +360,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const modules = this.getAllModulesList(this.sections).filter((module) => module.totalSize && module.totalSize > 0);
 | 
			
		||||
        const modules = CoreCourse.getSectionsModules(this.sections)
 | 
			
		||||
            .filter((module) => module.totalSize && module.totalSize > 0);
 | 
			
		||||
 | 
			
		||||
        await this.deleteModules(modules);
 | 
			
		||||
    }
 | 
			
		||||
@ -420,22 +391,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const modules: AddonStorageManagerModule[] = [];
 | 
			
		||||
        section.modules.forEach((module) => {
 | 
			
		||||
            if (module.subSection) {
 | 
			
		||||
                module.subSection.modules.forEach((module) => {
 | 
			
		||||
                    if (module.totalSize && module.totalSize > 0) {
 | 
			
		||||
                        modules.push(module);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (module.totalSize && module.totalSize > 0) {
 | 
			
		||||
                modules.push(module);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        const modules = CoreCourse.getSectionsModules([section]).filter((module) => module.totalSize && module.totalSize > 0);
 | 
			
		||||
 | 
			
		||||
        await this.deleteModules(modules);
 | 
			
		||||
    }
 | 
			
		||||
@ -481,16 +437,17 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
    protected async deleteModules(modules: AddonStorageManagerModule[]): Promise<void> {
 | 
			
		||||
        const modal = await CoreLoadings.show('core.deleting', true);
 | 
			
		||||
 | 
			
		||||
        const sections: AddonStorageManagerCourseSection[]  = [];
 | 
			
		||||
        const sections = new Set<AddonStorageManagerCourseSection>();
 | 
			
		||||
        const promises = modules.map(async (module) => {
 | 
			
		||||
            // Remove the files.
 | 
			
		||||
            await CoreCourseHelper.removeModuleStoredData(module, this.courseId);
 | 
			
		||||
 | 
			
		||||
            module.totalSize = 0;
 | 
			
		||||
 | 
			
		||||
            const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, module.section);
 | 
			
		||||
            if (sectionFinder?.section) {
 | 
			
		||||
                sections.push(sectionFinder?.section);
 | 
			
		||||
            const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section });
 | 
			
		||||
            const rootSection = parents[0] ?? section;
 | 
			
		||||
            if (rootSection && !sections.has(rootSection)) {
 | 
			
		||||
                sections.add(rootSection);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -501,7 +458,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
        } finally {
 | 
			
		||||
            modal.dismiss();
 | 
			
		||||
 | 
			
		||||
            await this.updateSizes(sections);
 | 
			
		||||
            await this.updateSizes(Array.from(sections));
 | 
			
		||||
 | 
			
		||||
            this.changeDetectorRef.markForCheck();
 | 
			
		||||
        }
 | 
			
		||||
@ -583,9 +540,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
        } finally {
 | 
			
		||||
            module.spinner = false;
 | 
			
		||||
 | 
			
		||||
            const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, module.section);
 | 
			
		||||
            if (sectionFinder?.section) {
 | 
			
		||||
                await this.updateSizes([sectionFinder?.section]);
 | 
			
		||||
            const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section });
 | 
			
		||||
            const rootSection = parents[0] ?? section;
 | 
			
		||||
            if (rootSection) {
 | 
			
		||||
                await this.updateSizes([rootSection]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -614,18 +572,20 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
     * @param section Section to check.
 | 
			
		||||
     */
 | 
			
		||||
    protected async calculateModulesStatusOnSection(section: AddonStorageManagerCourseSection): Promise<void> {
 | 
			
		||||
        await Promise.all(section.modules.map(async (module) => {
 | 
			
		||||
            if (module.handlerData?.showDownloadButton) {
 | 
			
		||||
                module.spinner = true;
 | 
			
		||||
                // Listen for changes on this module status, even if download isn't enabled.
 | 
			
		||||
                module.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(module.modname);
 | 
			
		||||
                const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId);
 | 
			
		||||
        await Promise.all(section.contents.map(async (modOrSubsection) => {
 | 
			
		||||
            if (!sectionContentIsModule(modOrSubsection)) {
 | 
			
		||||
                await this.calculateModulesStatusOnSection(modOrSubsection);
 | 
			
		||||
 | 
			
		||||
                this.updateModuleStatus(module, status);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (module.subSection) {
 | 
			
		||||
                await this.calculateModulesStatusOnSection(module.subSection);
 | 
			
		||||
            if (modOrSubsection.handlerData?.showDownloadButton) {
 | 
			
		||||
                modOrSubsection.spinner = true;
 | 
			
		||||
                // Listen for changes on this module status, even if download isn't enabled.
 | 
			
		||||
                modOrSubsection.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(modOrSubsection.modname);
 | 
			
		||||
                const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(modOrSubsection, this.courseId);
 | 
			
		||||
 | 
			
		||||
                this.updateModuleStatus(modOrSubsection, status);
 | 
			
		||||
            }
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
@ -714,29 +674,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all modules list.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sections Sections to get the modules from.
 | 
			
		||||
     * @returns All modules list.
 | 
			
		||||
     */
 | 
			
		||||
    protected getAllModulesList(sections: AddonStorageManagerCourseSection[]): AddonStorageManagerModule[] {
 | 
			
		||||
        const modules: AddonStorageManagerModule[] = [];
 | 
			
		||||
        sections.forEach((section) => {
 | 
			
		||||
            section.modules.forEach((module) => {
 | 
			
		||||
                modules.push(module);
 | 
			
		||||
 | 
			
		||||
                if (module.subSection) {
 | 
			
		||||
                    module.subSection.modules.forEach((module) => {
 | 
			
		||||
                        modules.push(module);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return modules;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate the size of the modules.
 | 
			
		||||
     *
 | 
			
		||||
@ -770,19 +707,16 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
     */
 | 
			
		||||
    accordionGroupChange(event?: AccordionGroupChangeEventDetail): void {
 | 
			
		||||
        const sectionIds = event?.value as string[] ?? this.accordionMultipleValue;
 | 
			
		||||
        this.sections.forEach((section) => {
 | 
			
		||||
        const allSections = CoreCourseHelper.flattenSections(this.sections);
 | 
			
		||||
        allSections.forEach((section) => {
 | 
			
		||||
            section.expanded = false;
 | 
			
		||||
            section.modules.forEach((section) => {
 | 
			
		||||
                if (section.subSection) {
 | 
			
		||||
                    section.subSection.expanded = false;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        sectionIds.forEach((sectionId) => {
 | 
			
		||||
            const sectionToExpand = CoreCourseHelper.findSectionById(this.sections, Number(sectionId));
 | 
			
		||||
            if (sectionToExpand) {
 | 
			
		||||
                sectionToExpand.expanded = true;
 | 
			
		||||
            const section = allSections.find((section) => section.id === Number(sectionId));
 | 
			
		||||
 | 
			
		||||
            if (section) {
 | 
			
		||||
                section.expanded = true;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@ -796,15 +730,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
        this.moduleStatusObserver?.off();
 | 
			
		||||
        this.siteUpdatedObserver?.off();
 | 
			
		||||
 | 
			
		||||
        this.sections.forEach((section) => {
 | 
			
		||||
            section.modules.forEach((module) => {
 | 
			
		||||
                module.subSection?.modules.forEach((module) => {
 | 
			
		||||
        CoreCourse.getSectionsModules(this.sections).forEach((module) => {
 | 
			
		||||
            module.handlerData?.onDestroy?.();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
                module.handlerData?.onDestroy?.();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        this.isDestroyed = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -813,9 +742,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
     *
 | 
			
		||||
     * @param sections Sections to calculate their status.
 | 
			
		||||
     */
 | 
			
		||||
    protected async calculateSectionsStatus(
 | 
			
		||||
        sections: AddonStorageManagerCourseSection[],
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
    protected async calculateSectionsStatus(sections: AddonStorageManagerCourseSection[]): Promise<void> {
 | 
			
		||||
        if (!sections) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -829,12 +756,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
                section.isCalculating = true;
 | 
			
		||||
                await this.calculateModulesStatusOnSection(section);
 | 
			
		||||
                await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false, false);
 | 
			
		||||
 | 
			
		||||
                await Promise.all(section.modules.map(async (module) => {
 | 
			
		||||
                    if (module.subSection) {
 | 
			
		||||
                        return CoreCourseHelper.calculateSectionStatus(module.subSection, this.courseId, false, false);
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
            } finally {
 | 
			
		||||
                section.isCalculating = false;
 | 
			
		||||
            }
 | 
			
		||||
@ -843,11 +764,11 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
 | 
			
		||||
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'contents'> & {
 | 
			
		||||
    totalSize: number;
 | 
			
		||||
    calculatingSize: boolean;
 | 
			
		||||
    expanded: boolean;
 | 
			
		||||
    modules: AddonStorageManagerModule[];
 | 
			
		||||
    contents: (AddonStorageManagerCourseSection | AddonStorageManagerModule)[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type AddonStorageManagerModule = CoreCourseModuleData & {
 | 
			
		||||
@ -856,5 +777,4 @@ type AddonStorageManagerModule = CoreCourseModuleData & {
 | 
			
		||||
    prefetchHandler?: CoreCourseModulePrefetchHandler;
 | 
			
		||||
    spinner?: boolean;
 | 
			
		||||
    downloadStatus?: DownloadStatus;
 | 
			
		||||
    subSection?: AddonStorageManagerCourseSection;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -239,7 +239,7 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy
 | 
			
		||||
     */
 | 
			
		||||
    private async calculateDownloadedCourseSize(courseId: number): Promise<number> {
 | 
			
		||||
        const sections = await CoreCourse.getSections(courseId);
 | 
			
		||||
        const modules = CoreCourseHelper.getSectionsModules(sections);
 | 
			
		||||
        const modules = CoreCourse.getSectionsModules(sections);
 | 
			
		||||
 | 
			
		||||
        return CoreCourseHelper.getModulesDownloadedSize(modules, courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -374,7 +374,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
                    section = lastModuleSection || section;
 | 
			
		||||
                    moduleId = lastModuleSection ? this.lastModuleViewed.cmId : undefined;
 | 
			
		||||
                } else {
 | 
			
		||||
                    const modules = CoreCourseHelper.getSectionsModules([currentSectionData.section]);
 | 
			
		||||
                    const modules = CoreCourse.getSectionsModules([currentSectionData.section]);
 | 
			
		||||
                    if (modules.some(module => module.id === this.lastModuleViewed?.cmId)) {
 | 
			
		||||
                        // Last module viewed is inside the highlighted section.
 | 
			
		||||
                        moduleId = this.lastModuleViewed.cmId;
 | 
			
		||||
@ -665,7 +665,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const sectionModules = CoreCourseHelper.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
 | 
			
		||||
            const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
 | 
			
		||||
 | 
			
		||||
            modulesLoaded += sectionModules.reduce((total, module) =>
 | 
			
		||||
                !CoreCourseHelper.isModuleStealth(module, this.sections[this.lastShownSectionIndex]) ? total + 1 : total, 0);
 | 
			
		||||
@ -817,9 +817,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        sectionIds?.forEach((sectionId) => {
 | 
			
		||||
            const sId = Number(sectionId);
 | 
			
		||||
            const section = allSections.find((section) => section.id === sId);
 | 
			
		||||
 | 
			
		||||
            const section = allSections.find((section) => section.id === Number(sectionId));
 | 
			
		||||
            if (section) {
 | 
			
		||||
                section.expanded = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -102,7 +102,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        const sections = await CoreCourse.getSections(this.courseId, false, true, preSets);
 | 
			
		||||
 | 
			
		||||
        const modules = await CoreCourseHelper.getSectionsModules(sections, {
 | 
			
		||||
        const modules = await CoreCourse.getSectionsModules(sections, {
 | 
			
		||||
            ignoreSection: (section) => !this.isSectionAvailable(section),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -115,7 +115,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
			
		||||
        if (checkNext) {
 | 
			
		||||
            // Find next Module.
 | 
			
		||||
            this.nextModule = undefined;
 | 
			
		||||
            for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule == undefined; i++) {
 | 
			
		||||
            for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule === undefined; i++) {
 | 
			
		||||
                const module = modules[i];
 | 
			
		||||
                if (this.isModuleAvailable(module)) {
 | 
			
		||||
                    this.nextModule = module;
 | 
			
		||||
@ -126,7 +126,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
			
		||||
        if (checkPrevious) {
 | 
			
		||||
            // Find previous Module.
 | 
			
		||||
            this.previousModule = undefined;
 | 
			
		||||
            for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule == undefined; i--) {
 | 
			
		||||
            for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule === undefined; i--) {
 | 
			
		||||
                const module = modules[i];
 | 
			
		||||
                if (this.isModuleAvailable(module)) {
 | 
			
		||||
                    this.previousModule = module;
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import {
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourseHelper,
 | 
			
		||||
    CoreCourseModuleCompletionData,
 | 
			
		||||
    CoreCourseModuleData,
 | 
			
		||||
    CoreCourseSection,
 | 
			
		||||
} from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
 | 
			
		||||
@ -215,10 +216,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
 | 
			
		||||
    protected async loadSections(refresh?: boolean): Promise<void> {
 | 
			
		||||
        // Get all the sections.
 | 
			
		||||
        const sections = await CoreCourse.getSections(this.course.id, false, true);
 | 
			
		||||
        let modules: CoreCourseModuleData[] | undefined;
 | 
			
		||||
 | 
			
		||||
        if (refresh) {
 | 
			
		||||
            // Invalidate the recently downloaded module list. To ensure info can be prefetched.
 | 
			
		||||
            const modules = CoreCourseHelper.getSectionsModules(sections);
 | 
			
		||||
            modules = CoreCourse.getSectionsModules(sections);
 | 
			
		||||
 | 
			
		||||
            await CoreCourseModulePrefetchDelegate.invalidateModules(modules, this.course.id);
 | 
			
		||||
        }
 | 
			
		||||
@ -227,7 +229,9 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
 | 
			
		||||
 | 
			
		||||
        // Get the completion status.
 | 
			
		||||
        if (CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) {
 | 
			
		||||
            const modules = CoreCourseHelper.getSectionsModules(sections);
 | 
			
		||||
            if (!modules) {
 | 
			
		||||
                modules = CoreCourse.getSectionsModules(sections);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (modules[0]?.completion !== undefined) {
 | 
			
		||||
                // The module already has completion (3.6 onwards). Load the offline completion.
 | 
			
		||||
@ -334,7 +338,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
 | 
			
		||||
 | 
			
		||||
            if (this.sections) {
 | 
			
		||||
                // If the completion value is not used, the page won't be reloaded, so update the progress bar.
 | 
			
		||||
                const completionModules = CoreCourseHelper.getSectionsModules(this.sections)
 | 
			
		||||
                const completionModules = CoreCourse.getSectionsModules(this.sections)
 | 
			
		||||
                    .map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
 | 
			
		||||
                    .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -175,7 +175,7 @@ export class CoreCourseListModTypePage implements OnInit {
 | 
			
		||||
        while (this.lastShownSectionIndex < this.sections.length - 1 && modulesLoaded < CoreCourseListModTypePage.PAGE_LENGTH) {
 | 
			
		||||
            this.lastShownSectionIndex++;
 | 
			
		||||
 | 
			
		||||
            const sectionModules = CoreCourseHelper.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
 | 
			
		||||
            const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
 | 
			
		||||
            modulesLoaded += sectionModules.length;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,6 @@ import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enr
 | 
			
		||||
import { LazyRoutesModule } from '@/app/app-routing.module';
 | 
			
		||||
import { CoreModals } from '@services/modals';
 | 
			
		||||
import { CoreLoadings } from '@services/loadings';
 | 
			
		||||
import { ArrayElement } from '@/core/utils/types';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Prefetch info of a module.
 | 
			
		||||
@ -288,11 +287,11 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
            throw new CoreError('Invalid section');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const sectionWithStatus = <CoreCourseSectionWithStatus> section;
 | 
			
		||||
        // Get the status of this section based on their modules.
 | 
			
		||||
        const { modules, subsections } = CoreCourse.classifyContents(section.contents);
 | 
			
		||||
 | 
			
		||||
        // Get the status of this section.
 | 
			
		||||
        const statusData = await CoreCourseModulePrefetchDelegate.getModulesStatus(
 | 
			
		||||
            section.contents,
 | 
			
		||||
            modules,
 | 
			
		||||
            courseId,
 | 
			
		||||
            section.id,
 | 
			
		||||
            refresh,
 | 
			
		||||
@ -300,12 +299,20 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
            checkUpdates,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Now calculate status of subsections, and add them to the status data. Each subsection counts as 1 item in the section.
 | 
			
		||||
        await Promise.all(subsections.map(async (subsection) => {
 | 
			
		||||
            const subsectionStatus = await this.calculateSectionStatus(subsection, courseId, refresh, checkUpdates);
 | 
			
		||||
            statusData.total++;
 | 
			
		||||
            statusData.status = CoreFilepool.determinePackagesStatus(statusData.status, subsectionStatus.statusData.status);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Check if it's being downloaded.
 | 
			
		||||
        const downloadId = this.getSectionDownloadId(section);
 | 
			
		||||
        if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
 | 
			
		||||
            statusData.status = DownloadStatus.DOWNLOADING;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const sectionWithStatus = <CoreCourseSectionWithStatus> section;
 | 
			
		||||
        sectionWithStatus.downloadStatus = statusData.status;
 | 
			
		||||
 | 
			
		||||
        // Set this section data.
 | 
			
		||||
@ -404,42 +411,28 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
        let count = 0;
 | 
			
		||||
 | 
			
		||||
        const promises = courses.map(async (course) => {
 | 
			
		||||
            const subPromises: Promise<void>[] = [];
 | 
			
		||||
            let sections: CoreCourseWSSection[];
 | 
			
		||||
            let handlers: CoreCourseOptionsHandlerToDisplay[] = [];
 | 
			
		||||
            let menuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
 | 
			
		||||
            let success = true;
 | 
			
		||||
 | 
			
		||||
            // Get the sections and the handlers.
 | 
			
		||||
            subPromises.push(CoreCourse.getSections(course.id, false, true).then((courseSections) => {
 | 
			
		||||
                sections = courseSections;
 | 
			
		||||
            const [sections, handlers, menuHandlers] = await Promise.all([
 | 
			
		||||
                CoreCourse.getSections(course.id, false, true),
 | 
			
		||||
                CoreCourseOptionsDelegate.getHandlersToDisplay(course, false),
 | 
			
		||||
                CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            subPromises.push(CoreCourseOptionsDelegate.getHandlersToDisplay(course, false).then((cHandlers) => {
 | 
			
		||||
                handlers = cHandlers;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
            subPromises.push(CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false).then((mHandlers) => {
 | 
			
		||||
                menuHandlers = mHandlers;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            return Promise.all(subPromises).then(() => this.prefetchCourse(course, sections, handlers, menuHandlers, siteId))
 | 
			
		||||
                .catch((error) => {
 | 
			
		||||
            try {
 | 
			
		||||
                await this.prefetchCourse(course, sections, handlers, menuHandlers, siteId);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                success = false;
 | 
			
		||||
 | 
			
		||||
                throw error;
 | 
			
		||||
                }).finally(() => {
 | 
			
		||||
            } finally {
 | 
			
		||||
                // Course downloaded or failed, notify the progress.
 | 
			
		||||
                count++;
 | 
			
		||||
                if (options.onProgress) {
 | 
			
		||||
                    options.onProgress({ count: count, total: total, courseId: course.id, success: success });
 | 
			
		||||
                }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (options.onProgress) {
 | 
			
		||||
@ -469,20 +462,31 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
            total: true,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await Promise.all(sections.map(async (section) => {
 | 
			
		||||
        const getSectionSize = async (section: CoreCourseWSSection): Promise<CoreFileSizeSum> => {
 | 
			
		||||
            if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
 | 
			
		||||
                return;
 | 
			
		||||
                return { size: 0, total: true };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
 | 
			
		||||
            const { modules, subsections } = CoreCourse.classifyContents(section.contents);
 | 
			
		||||
 | 
			
		||||
            sizeSum.total = sizeSum.total && sectionSize.total;
 | 
			
		||||
            sizeSum.size += sectionSize.size;
 | 
			
		||||
            const [modulesSize, subsectionsSizes] = await Promise.all([
 | 
			
		||||
                CoreCourseModulePrefetchDelegate.getDownloadSize(modules, courseId),
 | 
			
		||||
                Promise.all(subsections.map((modOrSubsection) => getSectionSize(modOrSubsection))),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Check if the section has embedded files in the description.
 | 
			
		||||
            if (!hasEmbeddedFiles && CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0) {
 | 
			
		||||
                hasEmbeddedFiles = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return subsectionsSizes.concat(modulesSize).reduce((sizeSum, contentSize) => ({
 | 
			
		||||
                size: sizeSum.size + contentSize.size,
 | 
			
		||||
                total: sizeSum.total && contentSize.total,
 | 
			
		||||
            }), { size: 0, total: true });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await Promise.all(sections.map(async (section) => {
 | 
			
		||||
            await getSectionSize(section);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        if (hasEmbeddedFiles) {
 | 
			
		||||
@ -1586,7 +1590,7 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
            // Prefetch other data needed to render the course.
 | 
			
		||||
            promises.push(CoreCourses.getCoursesByField('id', course.id));
 | 
			
		||||
 | 
			
		||||
            const modules = this.getSectionsModules(sections);
 | 
			
		||||
            const modules = CoreCourse.getSectionsModules(sections);
 | 
			
		||||
            if (!modules.length || modules[0].completion === undefined) {
 | 
			
		||||
                promises.push(CoreCourse.getActivitiesCompletionStatus(course.id));
 | 
			
		||||
            }
 | 
			
		||||
@ -1646,7 +1650,7 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
     * @param updateAllSections Update all sections status
 | 
			
		||||
     */
 | 
			
		||||
    async prefetchSections(
 | 
			
		||||
        sections: (CoreCourseSectionWithStatus & CoreCourseSectionWithSubsections)[],
 | 
			
		||||
        sections: CoreCourseSectionWithStatus[],
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        updateAllSections = false,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
@ -1736,11 +1740,14 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
     * @returns Promise resolved when the section is prefetched.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise<void> {
 | 
			
		||||
        const { modules, subsections } = CoreCourse.classifyContents(section.contents);
 | 
			
		||||
 | 
			
		||||
        const syncAndPrefetchModules = async () => {
 | 
			
		||||
            // Sync the modules first.
 | 
			
		||||
        await CoreCourseModulePrefetchDelegate.syncModules(section.contents, courseId);
 | 
			
		||||
            await CoreCourseModulePrefetchDelegate.syncModules(modules, courseId);
 | 
			
		||||
 | 
			
		||||
            // Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded.
 | 
			
		||||
        const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(section.contents, courseId, section.id);
 | 
			
		||||
            const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(modules, courseId, section.id);
 | 
			
		||||
 | 
			
		||||
            if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) {
 | 
			
		||||
                // Section is downloaded or not downloadable, nothing to do.
 | 
			
		||||
@ -1748,6 +1755,12 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await this.prefetchSingleSection(section, result, courseId);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            syncAndPrefetchModules(),
 | 
			
		||||
            Promise.all(subsections.map(subsection => this.prefetchSingleSectionIfNeeded(subsection, courseId))),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1860,7 +1873,7 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
    async deleteCourseFiles(courseId: number): Promise<void> {
 | 
			
		||||
        const siteId = CoreSites.getCurrentSiteId();
 | 
			
		||||
        const sections = await CoreCourse.getSections(courseId);
 | 
			
		||||
        const modules = this.getSectionsModules(sections);
 | 
			
		||||
        const modules = CoreCourse.getSectionsModules(sections);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            ...modules.map((module) => this.removeModuleStoredData(module, courseId)),
 | 
			
		||||
@ -2116,44 +2129,6 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
        return { section: foundSection, parents: parents.reverse() };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of sections, returns the list of modules in the sections.
 | 
			
		||||
     * The modules are ordered in the order of appearance in the course.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sections Sections.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @returns Modules.
 | 
			
		||||
     */
 | 
			
		||||
    getSectionsModules<
 | 
			
		||||
        Section extends CoreCourseWSSection,
 | 
			
		||||
        Module = Extract<ArrayElement<Section['contents']>, CoreCourseModuleData>
 | 
			
		||||
    >(
 | 
			
		||||
        sections: Section[],
 | 
			
		||||
        options: CoreCourseGetSectionsModulesOptions<Section, Module> = {},
 | 
			
		||||
    ): Module[] {
 | 
			
		||||
        let modules: Module[] = [];
 | 
			
		||||
 | 
			
		||||
        sections.forEach((section) => {
 | 
			
		||||
            if (options.ignoreSection && options.ignoreSection(section)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            section.contents.forEach((modOrSubsection) => {
 | 
			
		||||
                if (sectionContentIsModule(modOrSubsection)) {
 | 
			
		||||
                    if (options.ignoreModule && options.ignoreModule(modOrSubsection as Module)) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    modules.push(modOrSubsection as Module);
 | 
			
		||||
                } else {
 | 
			
		||||
                    modules = modules.concat(this.getSectionsModules([modOrSubsection], options));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return modules;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of sections, returns the list of sections and subsections.
 | 
			
		||||
     *
 | 
			
		||||
@ -2284,11 +2259,3 @@ export type CoreCourseGuestAccessInfo = {
 | 
			
		||||
     */
 | 
			
		||||
    passwordRequired?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for get sections modules.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCourseGetSectionsModulesOptions<Section, Module> = {
 | 
			
		||||
    ignoreSection?: (section: Section) => boolean; // Function to filter sections. Return true to ignore it, false to use it.
 | 
			
		||||
    ignoreModule?: (module: Module) => boolean; // Function to filter module. Return true to ignore it, false to use it.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -64,6 +64,7 @@ import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-si
 | 
			
		||||
import { CoreLoadings } from '@services/loadings';
 | 
			
		||||
import { CoreArray } from '@singletons/array';
 | 
			
		||||
import { CoreText } from '@singletons/text';
 | 
			
		||||
import { ArrayElement } from '@/core/utils/types';
 | 
			
		||||
 | 
			
		||||
const ROOT_CACHE_KEY = 'mmCourse:';
 | 
			
		||||
 | 
			
		||||
@ -1074,13 +1075,40 @@ export class CoreCourseProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of sections, returns the list of modules in the sections.
 | 
			
		||||
     * The modules are ordered in the order of appearance in the course.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sections Sections.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @returns Modules.
 | 
			
		||||
     * @deprecated since 4.5. Use CoreCourseHelper.getSectionsModules instead.
 | 
			
		||||
     */
 | 
			
		||||
    getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] {
 | 
			
		||||
        return CoreCourseHelper.getSectionsModules(sections);
 | 
			
		||||
    getSectionsModules<
 | 
			
		||||
        Section extends CoreCourseWSSection,
 | 
			
		||||
        Module = Extract<ArrayElement<Section['contents']>, CoreCourseModuleData>
 | 
			
		||||
    >(
 | 
			
		||||
        sections: Section[],
 | 
			
		||||
        options: CoreCourseGetSectionsModulesOptions<Section, Module> = {},
 | 
			
		||||
    ): Module[] {
 | 
			
		||||
        let modules: Module[] = [];
 | 
			
		||||
 | 
			
		||||
        sections.forEach((section) => {
 | 
			
		||||
            if (options.ignoreSection && options.ignoreSection(section)) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            section.contents.forEach((modOrSubsection) => {
 | 
			
		||||
                if (sectionContentIsModule(modOrSubsection)) {
 | 
			
		||||
                    if (options.ignoreModule && options.ignoreModule(modOrSubsection as Module)) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    modules.push(modOrSubsection as Module);
 | 
			
		||||
                } else {
 | 
			
		||||
                    modules = modules.concat(this.getSectionsModules([modOrSubsection], options));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return modules;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1635,6 +1663,31 @@ export class CoreCourseProvider {
 | 
			
		||||
        return CoreDomUtils.removeElementFromHtml(availabilityInfo, 'li[data-action="showmore"]');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given section contents, classify them into modules and sections.
 | 
			
		||||
     *
 | 
			
		||||
     * @param contents Contents.
 | 
			
		||||
     * @returns Classified contents.
 | 
			
		||||
     */
 | 
			
		||||
    classifyContents<
 | 
			
		||||
        Contents extends CoreCourseModuleOrSection,
 | 
			
		||||
        Module = Extract<Contents, CoreCourseModuleData>,
 | 
			
		||||
        Section = Extract<Contents, CoreCourseWSSection>,
 | 
			
		||||
    >(contents: Contents[]): { modules: Module[]; subsections: Section[] } {
 | 
			
		||||
        const modules: Module[] = [];
 | 
			
		||||
        const subsections: Section[] = [];
 | 
			
		||||
 | 
			
		||||
        contents.forEach((content) => {
 | 
			
		||||
            if (sectionContentIsModule(content)) {
 | 
			
		||||
                modules.push(content as Module);
 | 
			
		||||
            } else {
 | 
			
		||||
                subsections.push(content as unknown as Section);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return { modules, subsections };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CoreCourse = makeSingleton(CoreCourseProvider);
 | 
			
		||||
@ -2069,3 +2122,11 @@ export type CoreCourseGetSectionsOptions = CoreSitesCommonWSOptions & {
 | 
			
		||||
    includeStealthModules?: boolean; // Defaults to true.
 | 
			
		||||
    preSets?: CoreSiteWSPreSets;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for get sections modules.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCourseGetSectionsModulesOptions<Section, Module> = {
 | 
			
		||||
    ignoreSection?: (section: Section) => boolean; // Function to filter sections. Return true to ignore it, false to use it.
 | 
			
		||||
    ignoreModule?: (module: Module) => boolean; // Function to filter module. Return true to ignore it, false to use it.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user