commit
325aca7536
|
@ -70,16 +70,13 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
|
|||
let modFullNames: Record<string, string> = {};
|
||||
const brandedIcons: Record<string, boolean|undefined> = {};
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (!section.modules) {
|
||||
return;
|
||||
}
|
||||
const modules = CoreCourse.getSectionsModules(sections, {
|
||||
ignoreSection: section => !CoreCourseHelper.canUserViewSection(section),
|
||||
ignoreModule: module => !CoreCourseHelper.canUserViewModule(module) || !CoreCourse.moduleHasView(module),
|
||||
});
|
||||
|
||||
section.modules.forEach((mod) => {
|
||||
if (archetypes[mod.modname] !== undefined ||
|
||||
!CoreCourseHelper.canUserViewModule(mod, section) ||
|
||||
!CoreCourse.moduleHasView(mod)) {
|
||||
// Ignore this module.
|
||||
modules.forEach((mod) => {
|
||||
if (archetypes[mod.modname] !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -107,7 +104,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
|
|||
modIcons[mod.modname] = mod.modicon;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort the modnames alphabetically.
|
||||
modFullNames = CoreUtils.sortValues(modFullNames);
|
||||
for (const modName in modFullNames) {
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [section]="mainMenuBlock" />
|
||||
<ng-container *ngFor="let modOrSubsection of mainMenuBlock.contents">
|
||||
<core-course-module *ngIf="isModule(modOrSubsection)" [module]="modOrSubsection" [section]="mainMenuBlock" />
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourse, sectionContentIsModule } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||
|
@ -39,6 +39,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
|||
component = 'AddonBlockSiteMainMenu';
|
||||
mainMenuBlock?: CoreCourseSection;
|
||||
siteHomeId = 1;
|
||||
isModule = sectionContentIsModule;
|
||||
|
||||
protected fetchContentDefaultError = 'Error getting main menu data.';
|
||||
|
||||
|
@ -66,9 +67,12 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
|||
promises.push(CoreCourse.invalidateSections(this.siteHomeId));
|
||||
promises.push(CoreSiteHome.invalidateNewsForum(this.siteHomeId));
|
||||
|
||||
if (this.mainMenuBlock && this.mainMenuBlock.modules) {
|
||||
if (this.mainMenuBlock?.contents.length) {
|
||||
// Invalidate modules prefetch data.
|
||||
promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId));
|
||||
promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(
|
||||
CoreCourse.getSectionsModules([this.mainMenuBlock]),
|
||||
this.siteHomeId,
|
||||
));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
@ -114,11 +118,11 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
|||
try {
|
||||
const forum = await CoreSiteHome.getNewsForum(this.siteHomeId);
|
||||
// Search the module that belongs to site news.
|
||||
const forumIndex =
|
||||
this.mainMenuBlock.modules.findIndex((mod) => mod.modname == 'forum' && mod.instance == forum.id);
|
||||
const forumIndex = this.mainMenuBlock.contents.findIndex((mod) =>
|
||||
sectionContentIsModule(mod) && mod.modname == 'forum' && mod.instance == forum.id);
|
||||
|
||||
if (forumIndex >= 0) {
|
||||
this.mainMenuBlock.modules.splice(forumIndex, 1);
|
||||
this.mainMenuBlock.contents.splice(forumIndex, 1);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
|
|
|
@ -19,7 +19,8 @@ import { CoreCourse } from '@features/course/services/course';
|
|||
import { CoreLoadings } from '@services/loadings';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModSubsection } from '../subsection';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
|
||||
/**
|
||||
* Handler to treat links to subsection.
|
||||
|
@ -33,6 +34,29 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM
|
|||
super('AddonModSubsection', 'subsection', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a subsection.
|
||||
*
|
||||
* @param sectionId Section ID.
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async openSubsection(sectionId: number, courseId: number, siteId?: string): Promise<void> {
|
||||
const pageParams = {
|
||||
sectionId,
|
||||
};
|
||||
|
||||
if (
|
||||
(!siteId || siteId === CoreSites.getCurrentSiteId()) &&
|
||||
CoreCourse.currentViewIsCourse(courseId)
|
||||
) {
|
||||
CoreCourse.selectCourseTab('', pageParams);
|
||||
} else {
|
||||
await CoreCourseHelper.getAndOpenCourse(courseId, pageParams, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -51,7 +75,7 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM
|
|||
// Get the module.
|
||||
const module = await CoreCourse.getModule(moduleId, courseId, undefined, true, false, siteId);
|
||||
|
||||
await AddonModSubsection.openSubsection(module.section, module.course, siteId);
|
||||
await this.openSubsection(module.section, module.course, siteId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error opening link.');
|
||||
} finally {
|
||||
|
|
|
@ -1,88 +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 { CoreConstants, ModPurpose } from '@/core/constants';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
|
||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||
import {
|
||||
CoreCourseModuleDelegate,
|
||||
CoreCourseModuleHandler,
|
||||
CoreCourseModuleHandlerData,
|
||||
} from '@features/course/services/module-delegate';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModSubsection } from '../subsection';
|
||||
|
||||
/**
|
||||
* Handler to support subsection modules.
|
||||
*
|
||||
* This is merely to disable the siteplugin.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModSubsectionModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler {
|
||||
|
||||
name = 'AddonModSubsection';
|
||||
modName = 'subsection';
|
||||
|
||||
supportedFeatures = {
|
||||
[CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE,
|
||||
[CoreConstants.FEATURE_GROUPS]: false,
|
||||
[CoreConstants.FEATURE_GROUPINGS]: false,
|
||||
[CoreConstants.FEATURE_MOD_INTRO]: false,
|
||||
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
|
||||
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
|
||||
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
|
||||
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
||||
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: false,
|
||||
[CoreConstants.FEATURE_MOD_PURPOSE]: ModPurpose.MOD_PURPOSE_CONTENT,
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getData(module: CoreCourseModuleData): CoreCourseModuleHandlerData {
|
||||
return {
|
||||
icon: CoreCourseModuleDelegate.getModuleIconSrc(module.modname, module.modicon),
|
||||
title: module.name,
|
||||
a11yTitle: '',
|
||||
class: 'addon-mod-subsection-handler',
|
||||
hasCustomCmListItem: true,
|
||||
action: async(event, module) => {
|
||||
try {
|
||||
await AddonModSubsection.openSubsection(module.section, module.course);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error opening subsection.');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getMainComponent(): Promise<undefined> {
|
||||
// There's no need to implement this because subsection cannot be used in singleactivity course format.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getIconSrc(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModSubsectionModuleHandler = makeSingleton(AddonModSubsectionModuleHandlerService);
|
|
@ -1,51 +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 { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
* Service that provides some features for subsections.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModSubsectionProvider {
|
||||
|
||||
/**
|
||||
* Open a subsection.
|
||||
*
|
||||
* @param sectionId Section ID.
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async openSubsection(sectionId: number, courseId: number, siteId?: string): Promise<void> {
|
||||
const pageParams = {
|
||||
sectionId,
|
||||
};
|
||||
|
||||
if (
|
||||
(!siteId || siteId === CoreSites.getCurrentSiteId()) &&
|
||||
CoreCourse.currentViewIsCourse(courseId)
|
||||
) {
|
||||
CoreCourse.selectCourseTab('', pageParams);
|
||||
} else {
|
||||
await CoreCourseHelper.getAndOpenCourse(courseId, pageParams, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModSubsection = makeSingleton(AddonModSubsectionProvider);
|
|
@ -14,9 +14,7 @@
|
|||
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link';
|
||||
import { AddonModSubsectionModuleHandler } from './services/handlers/module';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
|
@ -24,7 +22,6 @@ import { AddonModSubsectionModuleHandler } from './services/handlers/module';
|
|||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
CoreCourseModuleDelegate.registerHandler(AddonModSubsectionModuleHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
<ion-card class="wholecourse">
|
||||
<ion-card-header>
|
||||
<ion-card-title>
|
||||
|
@ -46,9 +45,18 @@
|
|||
</ion-card-header>
|
||||
</ion-card>
|
||||
|
||||
<ion-accordion-group [multiple]="true" (ionChange)="accordionGroupChange($event.detail)" #accordionGroup>
|
||||
<ion-accordion-group [multiple]="true" (ionChange)="accordionGroupChange($event.detail)" [value]="accordionMultipleValue">
|
||||
<ng-container *ngFor="let section of sections">
|
||||
<ion-card class="section" *ngIf="section.modules.length > 0">
|
||||
<ng-container *ngTemplateOutlet="sectionCard; context: { section }" />
|
||||
</ng-container>
|
||||
</ion-accordion-group>
|
||||
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
|
||||
<ng-template #sectionCard let-section="section">
|
||||
<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>
|
||||
|
@ -66,27 +74,27 @@
|
|||
</ion-badge>
|
||||
<!-- Download progress. -->
|
||||
<p *ngIf="downloadEnabled && section.isDownloading">
|
||||
<core-progress-bar [progress]="section.total === 0 ? -1 : (section.count / section.total) * 100" />
|
||||
<core-progress-bar
|
||||
[progress]="section.total === undefined || section.total === 0 ? -1 : (section.count / section.total) * 100" />
|
||||
</p>
|
||||
</ion-label>
|
||||
<div class="storage-buttons" slot="end"
|
||||
*ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
|
||||
<div class="storage-buttons" slot="end" *ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
|
||||
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
|
||||
<core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus !== statusDownloaded"
|
||||
[status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
|
||||
[status]="section.downloadStatus" [enabled]="true" (action)="prefetchSection(section)"
|
||||
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true"
|
||||
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
|
||||
[statusSubject]="section.name" />
|
||||
|
||||
<ion-badge class="core-course-download-section-progress"
|
||||
*ngIf="section.isDownloading && section.count < section.total" role="progressbar"
|
||||
*ngIf="section.isDownloading && section.total !== undefined && section.count < section.total" role="progressbar"
|
||||
[attr.aria-valuemax]="section.total" [attr.aria-valuenow]="section.count"
|
||||
[attr.aria-valuetext]="'core.course.downloadsectionprogressdescription' | translate:section">
|
||||
{{section.count}} / {{section.total}}
|
||||
</ion-badge>
|
||||
</div>
|
||||
<ion-button (click)="deleteForSection($event, section)"
|
||||
*ngIf="!section.calculatingSize && section.totalSize > 0" color="danger" fill="clear">
|
||||
<ion-button (click)="deleteForSection($event, section)" *ngIf="!section.calculatingSize && section.totalSize > 0"
|
||||
color="danger" fill="clear">
|
||||
<ion-icon name="fas-trash" slot="icon-only"
|
||||
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }" />
|
||||
</ion-button>
|
||||
|
@ -94,53 +102,54 @@
|
|||
</ion-item>
|
||||
<ion-card-content slot="content">
|
||||
<ng-container *ngIf="section.expanded">
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<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, section)"
|
||||
*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>
|
||||
</ion-item>
|
||||
}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-card-content>
|
||||
</ion-accordion>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
</ion-accordion-group>
|
||||
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
</ng-template>
|
||||
|
|
|
@ -17,11 +17,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.accordion-expanded {
|
||||
ion-item.card-header {
|
||||
--border-width: 0 0 1px 0;
|
||||
}
|
||||
}
|
||||
ion-card-content {
|
||||
padding: 0;
|
||||
|
||||
|
|
|
@ -13,11 +13,12 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { CoreConstants, DownloadStatus } from '@/core/constants';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CoreCourse, CoreCourseProvider, sectionContentIsModule } from '@features/course/services/course';
|
||||
import {
|
||||
CoreCourseHelper,
|
||||
CoreCourseModuleData,
|
||||
CoreCourseSection,
|
||||
CoreCourseSectionWithStatus,
|
||||
CorePrefetchStatusInfo,
|
||||
} from '@features/course/services/course-helper';
|
||||
|
@ -25,12 +26,11 @@ import {
|
|||
CoreCourseModulePrefetchDelegate,
|
||||
CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
|
||||
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { AccordionGroupChangeEventDetail, IonAccordionGroup } from '@ionic/angular';
|
||||
import { AccordionGroupChangeEventDetail } from '@ionic/angular';
|
||||
import { CoreLoadings } from '@services/loadings';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreDom } from '@singletons/dom';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
@ -47,14 +47,13 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|||
})
|
||||
export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild('accordionGroup', { static: true }) accordionGroup!: IonAccordionGroup;
|
||||
|
||||
courseId!: number;
|
||||
title = '';
|
||||
loaded = false;
|
||||
sections: AddonStorageManagerCourseSection[] = [];
|
||||
totalSize = 0;
|
||||
calculatingSize = true;
|
||||
accordionMultipleValue: string[] = [];
|
||||
|
||||
downloadEnabled = false;
|
||||
downloadCourseEnabled = false;
|
||||
|
@ -67,6 +66,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
statusDownloaded = DownloadStatus.DOWNLOADED;
|
||||
isModule = sectionContentIsModule;
|
||||
|
||||
protected siteUpdatedObserver?: CoreEventObserver;
|
||||
protected courseStatusObserver?: CoreEventObserver;
|
||||
|
@ -116,27 +116,25 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
|
||||
const sections = (await CoreCourse.getSections(this.courseId, false, true))
|
||||
.filter((section) => !CoreCourseHelper.isSectionStealth(section));
|
||||
|
||||
this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
|
||||
.map(section => ({
|
||||
...section,
|
||||
totalSize: 0,
|
||||
calculatingSize: true,
|
||||
expanded: section.id === initialSectionId,
|
||||
modules: section.modules.map(module => ({
|
||||
...module,
|
||||
calculatingSize: true,
|
||||
})),
|
||||
}));
|
||||
.map(section => this.formatSection(section));
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
this.accordionGroup.value = String(initialSectionId);
|
||||
if (initialSectionId !== undefined && initialSectionId > 0) {
|
||||
this.accordionMultipleValue.push(initialSectionId.toString());
|
||||
this.accordionGroupChange();
|
||||
|
||||
CoreDom.scrollToElement(
|
||||
this.elementRef.nativeElement,
|
||||
'.accordion-expanded',
|
||||
`#addons-course-storage-${initialSectionId}`,
|
||||
{ addYAxis: -10 },
|
||||
);
|
||||
} else {
|
||||
this.accordionMultipleValue.push(this.sections[0].id.toString());
|
||||
this.accordionGroupChange();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.initSizes(),
|
||||
|
@ -147,9 +145,34 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Init course prefetch information.
|
||||
* Format a section.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @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.
|
||||
*/
|
||||
protected async initCoursePrefetch(): Promise<void> {
|
||||
if (!this.downloadCourseEnabled || this.courseStatusObserver) {
|
||||
|
@ -158,7 +181,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
|
||||
// Listen for changes in course status.
|
||||
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
|
||||
if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||
if (data.courseId === this.courseId || data.courseId === CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||
this.updateCourseStatus(data.status);
|
||||
}
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
@ -166,7 +189,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
// Determine the course prefetch status.
|
||||
await this.determineCoursePrefetchIcon();
|
||||
|
||||
if (this.prefetchCourseData.icon != CoreConstants.ICON_LOADING) {
|
||||
if (this.prefetchCourseData.icon !== CoreConstants.ICON_LOADING) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -189,8 +212,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
|
||||
/**
|
||||
* Init module prefetch information.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async initModulePrefetch(): Promise<void> {
|
||||
if (!this.downloadEnabled || this.sectionStatusObserver) {
|
||||
|
@ -205,154 +226,113 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
// 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.
|
||||
const downloadId = CoreCourseHelper.getSectionDownloadId({ id: data.sectionId });
|
||||
if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the affected section.
|
||||
const section = this.sections.find(section => section.id == data.sectionId);
|
||||
const { section } = CoreCourseHelper.findSection(this.sections, { id: data.sectionId });
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @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.
|
||||
const downloadId = CoreCourseHelper.getSectionDownloadId({ id: section.id });
|
||||
if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recalculate the status.
|
||||
await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false);
|
||||
await this.updateSizes([section]);
|
||||
|
||||
if (section.isDownloading && !CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||
// All the modules are now downloading, set a download all promise.
|
||||
this.prefecthSection(section);
|
||||
this.prefetchSection(section);
|
||||
}
|
||||
},
|
||||
CoreSites.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
// The download status of a section might have been changed from within a module page.
|
||||
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
|
||||
|
||||
this.sections.forEach((section) => {
|
||||
section.modules.forEach((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);
|
||||
this.calculateModuleStatus(module);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
||||
let module: AddonStorageManagerModule | undefined;
|
||||
const modules = CoreCourse.getSectionsModules(this.sections);
|
||||
const moduleFound = modules.find(module => module.id === data.componentId && module.prefetchHandler &&
|
||||
data.component === module.prefetchHandler?.component);
|
||||
|
||||
this.sections.some((section) => {
|
||||
module = section.modules.find((module) =>
|
||||
module.id == data.componentId && module.prefetchHandler && data.component == module.prefetchHandler?.component);
|
||||
|
||||
return !!module;
|
||||
});
|
||||
|
||||
if (!module) {
|
||||
if (!moduleFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call determineModuleStatus to get the right status to display.
|
||||
const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(module, data.status);
|
||||
const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(moduleFound, data.status);
|
||||
|
||||
// Update the status.
|
||||
this.updateModuleStatus(module, status);
|
||||
this.updateModuleStatus(moduleFound, status);
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
||||
// The download status of a section might have been changed from within a module page.
|
||||
this.updateSizes(this.sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init section, course and modules sizes.
|
||||
*/
|
||||
protected async initSizes(): Promise<void> {
|
||||
await Promise.all(this.sections.map(async (section) => {
|
||||
await Promise.all(section.modules.map(async (module) => {
|
||||
// Note: This function only gets the size for modules which are downloadable.
|
||||
// For other modules it always returns 0, even if they have downloaded some files.
|
||||
// However there is no 100% reliable way to actually track the files in this case.
|
||||
// You can maybe guess it based on the component and componentid.
|
||||
// But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
|
||||
// There is nothing enforcing correct values.
|
||||
// Most modules which have large files are downloadable, so I think this is sufficient.
|
||||
const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
|
||||
|
||||
// There are some cases where the return from this is not a valid number.
|
||||
if (!isNaN(size)) {
|
||||
module.totalSize = Number(size);
|
||||
section.totalSize += size;
|
||||
this.totalSize += size;
|
||||
}
|
||||
|
||||
module.calculatingSize = false;
|
||||
}));
|
||||
|
||||
section.calculatingSize = false;
|
||||
}));
|
||||
|
||||
this.calculatingSize = false;
|
||||
|
||||
// Mark course as not downloaded if course size is 0.
|
||||
if (this.totalSize == 0) {
|
||||
this.markCourseAsNotDownloaded();
|
||||
}
|
||||
await this.updateSizes(this.sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sizes of some modules.
|
||||
* Update the sizes of some sections and modules.
|
||||
*
|
||||
* @param modules Modules.
|
||||
* @param section Section the modules belong to.
|
||||
* @returns Promise resolved when done.
|
||||
* @param sections Modules.
|
||||
*/
|
||||
protected async updateModulesSizes(
|
||||
modules: AddonStorageManagerModule[],
|
||||
section?: AddonStorageManagerCourseSection,
|
||||
): Promise<void> {
|
||||
protected async updateSizes(sections: AddonStorageManagerCourseSection[]): Promise<void> {
|
||||
this.calculatingSize = true;
|
||||
|
||||
await Promise.all(modules.map(async (module) => {
|
||||
if (module.calculatingSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
module.calculatingSize = true;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
if (!section) {
|
||||
section = this.sections.find((section) => section.modules.some((mod) => mod.id === module.id));
|
||||
if (section) {
|
||||
CoreCourseHelper.flattenSections(sections).forEach((section) => {
|
||||
section.calculatingSize = true;
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
|
||||
|
||||
const diff = (isNaN(size) ? 0 : size) - (module.totalSize ?? 0);
|
||||
|
||||
module.totalSize = Number(size);
|
||||
this.totalSize += diff;
|
||||
if (section) {
|
||||
section.totalSize += diff;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, it shouldn't happen.
|
||||
} finally {
|
||||
module.calculatingSize = false;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
// Update only affected module sections.
|
||||
const modules = CoreCourse.getSectionsModules(sections);
|
||||
await Promise.all(modules.map(async (module) => {
|
||||
await this.calculateModuleSize(module);
|
||||
}));
|
||||
|
||||
this.calculatingSize = false;
|
||||
if (section) {
|
||||
section.calculatingSize = false;
|
||||
const updateSectionSize = (section: AddonStorageManagerCourseSection): void => {
|
||||
section.totalSize = 0;
|
||||
section.calculatingSize = true;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
section.contents.forEach((modOrSubsection) => {
|
||||
if (!sectionContentIsModule(modOrSubsection)) {
|
||||
updateSectionSize(modOrSubsection);
|
||||
}
|
||||
|
||||
section.totalSize += modOrSubsection.totalSize ?? 0;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
section.calculatingSize = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
};
|
||||
|
||||
// Update section and total sizes.
|
||||
this.totalSize = 0;
|
||||
this.sections.forEach((section) => {
|
||||
updateSectionSize(section);
|
||||
this.totalSize += section.totalSize;
|
||||
});
|
||||
this.calculatingSize = false;
|
||||
|
||||
// Mark course as not downloaded if course size is 0.
|
||||
if (this.totalSize === 0) {
|
||||
this.markCourseAsNotDownloaded();
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
await this.calculateSectionsStatus(sections);
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
|
@ -380,16 +360,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
const modules: AddonStorageManagerModule[] = [];
|
||||
this.sections.forEach((section) => {
|
||||
section.modules.forEach((module) => {
|
||||
if (module.totalSize && module.totalSize > 0) {
|
||||
modules.push(module);
|
||||
}
|
||||
});
|
||||
});
|
||||
const modules = CoreCourse.getSectionsModules(this.sections)
|
||||
.filter((module) => module.totalSize && module.totalSize > 0);
|
||||
|
||||
this.deleteModules(modules);
|
||||
await this.deleteModules(modules);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -417,14 +391,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
const modules: AddonStorageManagerModule[] = [];
|
||||
section.modules.forEach((module) => {
|
||||
if (module.totalSize && module.totalSize > 0) {
|
||||
modules.push(module);
|
||||
}
|
||||
});
|
||||
const modules = CoreCourse.getSectionsModules([section]).filter((module) => module.totalSize && module.totalSize > 0);
|
||||
|
||||
this.deleteModules(modules, section);
|
||||
await this.deleteModules(modules);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -432,12 +401,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
*
|
||||
* @param event Event object.
|
||||
* @param module Module details
|
||||
* @param section Section the module belongs to.
|
||||
*/
|
||||
async deleteForModule(
|
||||
event: Event,
|
||||
module: AddonStorageManagerModule,
|
||||
section: AddonStorageManagerCourseSection,
|
||||
): Promise<void> {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
@ -459,35 +426,29 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
this.deleteModules([module], section);
|
||||
await this.deleteModules([module]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified modules, showing the loading overlay while it happens.
|
||||
*
|
||||
* @param modules Modules to delete
|
||||
* @param section Section the modules belong to.
|
||||
* @returns Promise<void> Once deleting has finished
|
||||
*/
|
||||
protected async deleteModules(modules: AddonStorageManagerModule[], section?: AddonStorageManagerCourseSection): Promise<void> {
|
||||
protected async deleteModules(modules: AddonStorageManagerModule[]): Promise<void> {
|
||||
const modal = await CoreLoadings.show('core.deleting', true);
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
modules.forEach((module) => {
|
||||
const sections = new Set<AddonStorageManagerCourseSection>();
|
||||
const promises = modules.map(async (module) => {
|
||||
// Remove the files.
|
||||
const promise = CoreCourseHelper.removeModuleStoredData(module, this.courseId).then(() => {
|
||||
const moduleSize = module.totalSize || 0;
|
||||
// When the files and cache are removed, update the size.
|
||||
if (section) {
|
||||
section.totalSize -= moduleSize;
|
||||
}
|
||||
this.totalSize -= moduleSize;
|
||||
await CoreCourseHelper.removeModuleStoredData(module, this.courseId);
|
||||
|
||||
module.totalSize = 0;
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section });
|
||||
const rootSection = parents[0] ?? section;
|
||||
if (rootSection && !sections.has(rootSection)) {
|
||||
sections.add(rootSection);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -497,13 +458,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
} finally {
|
||||
modal.dismiss();
|
||||
|
||||
await this.updateModulesSizes(modules, section);
|
||||
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
|
||||
await this.updateSizes(Array.from(sections));
|
||||
|
||||
// For delete all, reset all section sizes so icons are updated.
|
||||
if (this.totalSize === 0) {
|
||||
this.sections.map(section => section.totalSize = 0);
|
||||
}
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
@ -519,40 +475,26 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
CoreCourse.setCourseStatus(this.courseId, DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the status of sections.
|
||||
*
|
||||
* @param refresh If refresh or not.
|
||||
*/
|
||||
protected calculateSectionsStatus(refresh?: boolean): void {
|
||||
if (!this.sections) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoreUtils.ignoreErrors(CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, refresh));
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm and prefetch a section. If the section is "all sections", prefetch all the sections.
|
||||
*
|
||||
* @param section Section to download.
|
||||
*/
|
||||
async prefecthSection(section: AddonStorageManagerCourseSection): Promise<void> {
|
||||
async prefetchSection(section: AddonStorageManagerCourseSection): Promise<void> {
|
||||
section.isCalculating = true;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
try {
|
||||
await CoreCourseHelper.confirmDownloadSizeSection(this.courseId, section, this.sections);
|
||||
await CoreCourseHelper.confirmDownloadSizeSection(this.courseId, [section]);
|
||||
|
||||
try {
|
||||
await CoreCourseHelper.prefetchSection(section, this.courseId, this.sections);
|
||||
await CoreCourseHelper.prefetchSections([section], this.courseId);
|
||||
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
|
||||
}
|
||||
} finally {
|
||||
await this.updateModulesSizes(section.modules, section);
|
||||
this.changeDetectorRef.markForCheck();
|
||||
await this.updateSizes([section]);
|
||||
}
|
||||
} catch (error) {
|
||||
// User cancelled or there was an error calculating the size.
|
||||
|
@ -588,12 +530,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
|
||||
try {
|
||||
// Get download size to ask for confirm if it's high.
|
||||
|
||||
const size = await module.prefetchHandler.getDownloadSize(module, module.course, true);
|
||||
|
||||
await CoreCourseHelper.prefetchModule(module.prefetchHandler, module, size, module.course, refresh);
|
||||
|
||||
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
|
@ -601,8 +540,11 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
} finally {
|
||||
module.spinner = false;
|
||||
|
||||
await this.updateModulesSizes([module]);
|
||||
|
||||
const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section });
|
||||
const rootSection = parents[0] ?? section;
|
||||
if (rootSection) {
|
||||
await this.updateSizes([rootSection]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -625,25 +567,31 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculate and show module status.
|
||||
* Calculate all modules status on a section.
|
||||
*
|
||||
* @param module Module to update.
|
||||
* @returns Promise resolved when done.
|
||||
* @param section Section to check.
|
||||
*/
|
||||
protected async calculateModuleStatus(module: AddonStorageManagerModule): Promise<void> {
|
||||
if (!module) {
|
||||
protected async calculateModulesStatusOnSection(section: AddonStorageManagerCourseSection): Promise<void> {
|
||||
await Promise.all(section.contents.map(async (modOrSubsection) => {
|
||||
if (!sectionContentIsModule(modOrSubsection)) {
|
||||
await this.calculateModulesStatusOnSection(modOrSubsection);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId);
|
||||
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(module, status);
|
||||
this.updateModuleStatus(modOrSubsection, status);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the prefetch icon of the course.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async determineCoursePrefetchIcon(): Promise<void> {
|
||||
this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId);
|
||||
|
@ -664,6 +612,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course object.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @returns Promise resolved with the course object if found.
|
||||
*/
|
||||
protected async getCourse(courseId: number): Promise<CoreCourseAnyCourseData | undefined> {
|
||||
try {
|
||||
// Check if user is enrolled. If enrolled, no guest access.
|
||||
|
@ -709,8 +663,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
isGuest: this.isGuest,
|
||||
},
|
||||
);
|
||||
await Promise.all(this.sections.map(section => this.updateModulesSizes(section.modules, section)));
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
await this.updateSizes(this.sections);
|
||||
} catch (error) {
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
|
@ -720,17 +674,47 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the size of the modules.
|
||||
*
|
||||
* @param module Module to calculate.
|
||||
*/
|
||||
protected async calculateModuleSize(module: AddonStorageManagerModule): Promise<void> {
|
||||
if (module.calculatingSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
module.calculatingSize = true;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
// Note: This function only gets the size for modules which are downloadable.
|
||||
// For other modules it always returns 0, even if they have downloaded some files.
|
||||
// However there is no 100% reliable way to actually track the files in this case.
|
||||
// You can maybe guess it based on the component and componentid.
|
||||
// But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
|
||||
// There is nothing enforcing correct values.
|
||||
// Most modules which have large files are downloadable, so I think this is sufficient.
|
||||
module.totalSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
|
||||
|
||||
module.calculatingSize = false;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle expand status.
|
||||
*
|
||||
* @param event Event object.
|
||||
* @param event Event object. If not defined, use the current value.
|
||||
*/
|
||||
accordionGroupChange(event: AccordionGroupChangeEventDetail): void {
|
||||
this.sections.forEach((section) => {
|
||||
accordionGroupChange(event?: AccordionGroupChangeEventDetail): void {
|
||||
const sectionIds = event?.value as string[] ?? this.accordionMultipleValue;
|
||||
const allSections = CoreCourseHelper.flattenSections(this.sections);
|
||||
allSections.forEach((section) => {
|
||||
section.expanded = false;
|
||||
});
|
||||
event.value.forEach((sectionId) => {
|
||||
const section = this.sections.find((section) => section.id === Number(sectionId));
|
||||
|
||||
sectionIds.forEach((sectionId) => {
|
||||
const section = allSections.find((section) => section.id === Number(sectionId));
|
||||
|
||||
if (section) {
|
||||
section.expanded = true;
|
||||
}
|
||||
|
@ -746,21 +730,45 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
|||
this.moduleStatusObserver?.off();
|
||||
this.siteUpdatedObserver?.off();
|
||||
|
||||
this.sections.forEach((section) => {
|
||||
section.modules.forEach((module) => {
|
||||
CoreCourse.getSectionsModules(this.sections).forEach((module) => {
|
||||
module.handlerData?.onDestroy?.();
|
||||
});
|
||||
});
|
||||
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the status of a list of sections, setting attributes to determine the icons/data to be shown.
|
||||
*
|
||||
* @param sections Sections to calculate their status.
|
||||
*/
|
||||
protected async calculateSectionsStatus(sections: AddonStorageManagerCourseSection[]): Promise<void> {
|
||||
if (!sections) {
|
||||
return;
|
||||
}
|
||||
|
||||
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
|
||||
await Promise.all(sections.map(async (section) => {
|
||||
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
section.isCalculating = true;
|
||||
await this.calculateModulesStatusOnSection(section);
|
||||
await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false, false);
|
||||
} finally {
|
||||
section.isCalculating = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'contents'> & {
|
||||
totalSize: number;
|
||||
calculatingSize: boolean;
|
||||
expanded: boolean;
|
||||
modules: AddonStorageManagerModule[];
|
||||
contents: (AddonStorageManagerCourseSection | AddonStorageManagerModule)[];
|
||||
};
|
||||
|
||||
type AddonStorageManagerModule = CoreCourseModuleData & {
|
||||
|
|
|
@ -17,7 +17,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||
import { CoreQueueRunner } from '@classes/queue-runner';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
||||
import { CoreSettingsHelper, CoreSiteSpaceUsage } from '@features/settings/services/settings-helper';
|
||||
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
|
||||
|
@ -240,15 +239,9 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy
|
|||
*/
|
||||
private async calculateDownloadedCourseSize(courseId: number): Promise<number> {
|
||||
const sections = await CoreCourse.getSections(courseId);
|
||||
const modules = sections.map((section) => section.modules).flat();
|
||||
const promisedModuleSizes = modules.map(async (module) => {
|
||||
const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId);
|
||||
const modules = CoreCourse.getSectionsModules(sections);
|
||||
|
||||
return isNaN(size) ? 0 : size;
|
||||
});
|
||||
const moduleSizes = await Promise.all(promisedModuleSizes);
|
||||
|
||||
return moduleSizes.reduce((totalSize, moduleSize) => totalSize + moduleSize, 0);
|
||||
return CoreCourseHelper.getModulesDownloadedSize(modules, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
|
||||
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
|
||||
[value]="accordionMultipleValue">
|
||||
<core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId &&
|
||||
!selectedSection.component" [course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed"
|
||||
[viewedModules]="viewedModules" [collapsible]="false" [subSections]="subSections" />
|
||||
<core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId"
|
||||
[course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
|
||||
[collapsible]="false" />
|
||||
</ion-accordion-group>
|
||||
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
|
||||
[message]="'core.course.nocontentavailable' | translate" />
|
||||
|
@ -27,11 +27,10 @@
|
|||
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
|
||||
[value]="accordionMultipleValue">
|
||||
@for (section of sections; track section.id) {
|
||||
@if ($index <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !== allSectionsId &&
|
||||
section.id !== stealthModulesSectionId && !section.component) {
|
||||
<core-course-section
|
||||
[course]="course" [section]="section" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
|
||||
[collapsible]="true" [subSections]="subSections" />
|
||||
@if ($index
|
||||
<= lastShownSectionIndex && !section.hiddenbynumsections && section.id !==allSectionsId && section.id
|
||||
!==stealthModulesSectionId) { <core-course-section [course]="course" [section]="section"
|
||||
[lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules" [collapsible]="true" />
|
||||
}
|
||||
}
|
||||
</ion-accordion-group>
|
||||
|
|
|
@ -31,9 +31,11 @@ import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
|||
import {
|
||||
CoreCourse,
|
||||
CoreCourseProvider,
|
||||
sectionContentIsModule,
|
||||
} from '@features/course/services/course';
|
||||
import {
|
||||
CoreCourseHelper,
|
||||
CoreCourseModuleData,
|
||||
CoreCourseSection,
|
||||
} from '@features/course/services/course-helper';
|
||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||
|
@ -124,7 +126,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
displayCourseIndex = false;
|
||||
displayBlocks = false;
|
||||
hasBlocks = false;
|
||||
subSections: CoreCourseSectionToDisplay[] = []; // List of course subsections.
|
||||
selectedSection?: CoreCourseSectionToDisplay;
|
||||
previousSection?: CoreCourseSectionToDisplay;
|
||||
nextSection?: CoreCourseSectionToDisplay;
|
||||
|
@ -184,27 +185,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
this.modViewedObserver = CoreEvents.on(CoreEvents.COURSE_MODULE_VIEWED, (data) => {
|
||||
if (data.courseId !== this.course.id) {
|
||||
this.modViewedObserver = CoreEvents.on(CoreEvents.COURSE_MODULE_VIEWED, (lastModuleViewed) => {
|
||||
if (lastModuleViewed.courseId !== this.course.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewedModules[data.cmId] = true;
|
||||
if (!this.lastModuleViewed || data.timeaccess > this.lastModuleViewed.timeaccess) {
|
||||
this.lastModuleViewed = data;
|
||||
this.viewedModules[lastModuleViewed.cmId] = true;
|
||||
if (!this.lastModuleViewed || lastModuleViewed.timeaccess > this.lastModuleViewed.timeaccess) {
|
||||
this.lastModuleViewed = lastModuleViewed;
|
||||
|
||||
if (this.selectedSection && this.selectedSection.id !== this.allSectionsId) {
|
||||
// Change section to display the one with the last viewed module
|
||||
const lastViewedSection = this.getViewedModuleSection(this.sections, data);
|
||||
const lastViewedSection = this.getViewedModuleSection();
|
||||
if (lastViewedSection && lastViewedSection.id !== this.selectedSection?.id) {
|
||||
this.sectionChanged(lastViewedSection, data.cmId);
|
||||
this.sectionChanged(lastViewedSection, this.lastModuleViewed.cmId);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.initializeExpandedSections();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -226,11 +225,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
if (changes.sections && this.sections) {
|
||||
this.subSections = this.sections.filter((section) => section.component === 'mod_subsection');
|
||||
this.sections = this.sections.filter((section) => section.component !== 'mod_subsection');
|
||||
await this.initializeExpandedSections();
|
||||
|
||||
this.treatSections(this.sections);
|
||||
await this.treatSections(this.sections);
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
|
@ -249,7 +248,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* Get the components classes.
|
||||
*/
|
||||
protected async getComponents(): Promise<void> {
|
||||
if (!this.course || this.course.format == this.lastCourseFormat) {
|
||||
if (!this.course || this.course.format === this.lastCourseFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -326,35 +325,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.loaded = true;
|
||||
this.sectionChanged(sections[0]);
|
||||
} else if (this.initialSectionId || this.initialSectionNumber !== undefined) {
|
||||
const subSection = this.subSections.find((section) => section.id === this.initialSectionId ||
|
||||
(section.section !== undefined && section.section === this.initialSectionNumber));
|
||||
if (subSection) {
|
||||
// The section is a subsection, load the parent section.
|
||||
this.sections.some((section) => {
|
||||
const module = section.modules.find((module) =>
|
||||
subSection.itemid === module.instance && module.modname === 'subsection');
|
||||
if (module) {
|
||||
this.initialSectionId = module.section;
|
||||
this.initialSectionNumber = undefined;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// We have an input indicating the section ID to load. Search the section.
|
||||
const { section, parents } = CoreCourseHelper.findSection(this.sections, {
|
||||
id: this.initialSectionId,
|
||||
num: this.initialSectionNumber,
|
||||
});
|
||||
|
||||
if (parents.length) {
|
||||
// The section is a subsection, load the root section.
|
||||
this.initialSectionId = parents[0].id;
|
||||
this.initialSectionNumber = undefined;
|
||||
|
||||
this.setInputData();
|
||||
}
|
||||
|
||||
// We have an input indicating the section ID to load. Search the section.
|
||||
const section = sections.find((section) =>
|
||||
section.id === this.initialSectionId ||
|
||||
(section.section !== undefined && section.section === this.initialSectionNumber));
|
||||
|
||||
// Don't load the section if it cannot be viewed by the user.
|
||||
if (section && this.canViewSection(section)) {
|
||||
const sectionToLoad = parents[0] ?? section;
|
||||
if (sectionToLoad && this.canViewSection(sectionToLoad)) {
|
||||
this.loaded = true;
|
||||
this.sectionChanged(section);
|
||||
this.sectionChanged(sectionToLoad);
|
||||
}
|
||||
} else if (this.initialBlockInstanceId && this.displayBlocks && this.hasBlocks) {
|
||||
const { CoreBlockSideBlocksComponent } = await import('@features/block/components/side-blocks/side-blocks');
|
||||
|
@ -374,21 +363,22 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
// No section specified, not found or not visible, load current section or the section with last module viewed.
|
||||
const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
|
||||
|
||||
const lastModuleViewed = this.lastModuleViewed;
|
||||
let section = currentSectionData.section;
|
||||
let moduleId: number | undefined;
|
||||
|
||||
// If all sections is not preferred, load the last viewed module section.
|
||||
if (!allSectionsPreferred && lastModuleViewed) {
|
||||
if (!allSectionsPreferred && this.lastModuleViewed) {
|
||||
if (!currentSectionData.forceSelected) {
|
||||
// Search the section with the last module viewed.
|
||||
const lastModuleSection = this.getViewedModuleSection(sections, lastModuleViewed);
|
||||
|
||||
const lastModuleSection = this.getViewedModuleSection();
|
||||
section = lastModuleSection || section;
|
||||
moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined;
|
||||
} else if (currentSectionData.section.modules.some(module => module.id === lastModuleViewed.cmId)) {
|
||||
moduleId = lastModuleSection ? this.lastModuleViewed.cmId : undefined;
|
||||
} else {
|
||||
const modules = CoreCourse.getSectionsModules([currentSectionData.section]);
|
||||
if (modules.some(module => module.id === this.lastModuleViewed?.cmId)) {
|
||||
// Last module viewed is inside the highlighted section.
|
||||
moduleId = lastModuleViewed.cmId;
|
||||
moduleId = this.lastModuleViewed.cmId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -413,37 +403,29 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.viewedModules[entry.cmId] = true;
|
||||
});
|
||||
|
||||
if (this.lastModuleViewed) {
|
||||
const section = this.getViewedModuleSection(this.sections, this.lastModuleViewed);
|
||||
if (section) {
|
||||
this.setSectionExpanded(section);
|
||||
}
|
||||
const lastViewedSection = this.getViewedModuleSection();
|
||||
if (lastViewedSection) {
|
||||
this.setSectionExpanded(lastViewedSection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the section of a viewed module.
|
||||
* Get the section of a viewed module. If the module is in a subsection, returns the root section.
|
||||
*
|
||||
* @param sections List of sections.
|
||||
* @param viewedModule Viewed module.
|
||||
* @returns Section, undefined if not found.
|
||||
*/
|
||||
protected getViewedModuleSection(
|
||||
sections: CoreCourseSection[],
|
||||
viewedModule: CoreCourseViewedModulesDBRecord,
|
||||
): CoreCourseSection | undefined {
|
||||
let lastModuleSection: CoreCourseSection | undefined;
|
||||
|
||||
if (viewedModule.sectionId) {
|
||||
lastModuleSection = sections.find(section => section.id === viewedModule.sectionId);
|
||||
protected getViewedModuleSection(): CoreCourseSection | undefined {
|
||||
if (!this.lastModuleViewed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastModuleSection) {
|
||||
// No sectionId or section not found. Search the module.
|
||||
lastModuleSection = sections.find(section => section.modules.some(module => module.id === viewedModule.cmId));
|
||||
}
|
||||
const { section, parents } = CoreCourseHelper.findSection(this.sections, {
|
||||
id: this.lastModuleViewed.sectionId,
|
||||
moduleId: this.lastModuleViewed.cmId,
|
||||
});
|
||||
const lastModuleSection: CoreCourseSection | undefined = parents[0] ?? section;
|
||||
|
||||
return lastModuleSection && lastModuleSection.id !== this.stealthModulesSectionId ? lastModuleSection : undefined;
|
||||
return lastModuleSection?.id !== this.stealthModulesSectionId ? lastModuleSection : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -488,7 +470,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
componentProps: {
|
||||
course: this.course,
|
||||
sections: this.sections,
|
||||
subSections: this.subSections,
|
||||
selectedId: selectedId,
|
||||
},
|
||||
});
|
||||
|
@ -496,29 +477,32 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
if (!data) {
|
||||
return;
|
||||
}
|
||||
let section = this.sections.find((section) => section.id === data.sectionId);
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
this.sectionChanged(section);
|
||||
|
||||
if (data.subSectionId) {
|
||||
section = this.subSections.find((section) => section.id === data.subSectionId);
|
||||
if (!section) {
|
||||
return;
|
||||
const { section, parents } = CoreCourseHelper.findSection(this.sections, {
|
||||
moduleId: data.moduleId,
|
||||
id: data.moduleId === undefined ? data.sectionId : undefined,
|
||||
});
|
||||
|
||||
// Select the root section.
|
||||
this.sectionChanged(parents[0] ?? section);
|
||||
|
||||
if (parents.length && section) {
|
||||
// It's a subsection. Expand all the parents and the subsection.
|
||||
for (let i = 1; i < parents.length; i++) {
|
||||
this.setSectionExpanded(parents[i]);
|
||||
}
|
||||
|
||||
// Use this section to find the module.
|
||||
this.setSectionExpanded(section);
|
||||
|
||||
// Scroll to the subsection (later it may be scrolled to the module).
|
||||
this.scrollInCourse(section.id, true);
|
||||
}
|
||||
|
||||
if (!data.moduleId) {
|
||||
if (!data.moduleId || !section) {
|
||||
return;
|
||||
}
|
||||
const module = section.modules.find((module) => module.id === data.moduleId);
|
||||
const module = <CoreCourseModuleData | undefined>
|
||||
section.contents.find((module) => sectionContentIsModule(module) && module.id === data.moduleId);
|
||||
if (!module) {
|
||||
return;
|
||||
}
|
||||
|
@ -541,14 +525,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* Open course downloads page.
|
||||
*/
|
||||
async gotoCourseDownloads(): Promise<void> {
|
||||
const selectedId = await this.getSelectedSectionId();
|
||||
const sectionId = this.selectedSection?.id !== this.allSectionsId ? this.selectedSection?.id : undefined;
|
||||
|
||||
CoreNavigator.navigateToSitePath(
|
||||
`storage/${this.course.id}`,
|
||||
{
|
||||
params: {
|
||||
title: this.course.fullname,
|
||||
sectionId: selectedId,
|
||||
sectionId,
|
||||
isGuest: this.isGuest,
|
||||
},
|
||||
},
|
||||
|
@ -676,12 +660,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
// Skip sections without content, with stealth modules or collapsed.
|
||||
if (!this.sections[this.lastShownSectionIndex].hasContent ||
|
||||
!this.sections[this.lastShownSectionIndex].modules ||
|
||||
!this.sections[this.lastShownSectionIndex].contents ||
|
||||
!this.sections[this.lastShownSectionIndex].expanded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modulesLoaded += this.sections[this.lastShownSectionIndex].modules.reduce((total, module) =>
|
||||
const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
|
||||
|
||||
modulesLoaded += sectionModules.reduce((total, module) =>
|
||||
!CoreCourseHelper.isModuleStealth(module, this.sections[this.lastShownSectionIndex]) ? total + 1 : total, 0);
|
||||
}
|
||||
|
||||
|
@ -782,9 +768,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* Save expanded sections for the course.
|
||||
*/
|
||||
protected async saveExpandedSections(): Promise<void> {
|
||||
let expandedSections = this.sections.filter((section) => section.expanded && section.id > 0).map((section) => section.id);
|
||||
expandedSections =
|
||||
expandedSections.concat(this.subSections.filter((section) => section.expanded).map((section) => section.id));
|
||||
const expandedSections = CoreCourseHelper.flattenSections(this.sections)
|
||||
.filter((section) => section.expanded && section.id > 0).map((section) => section.id);
|
||||
|
||||
await this.currentSite?.setLocalSiteConfig(
|
||||
`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`,
|
||||
|
@ -800,14 +785,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.currentSite?.getLocalSiteConfig<string>(`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`),
|
||||
);
|
||||
|
||||
// Expand all sections if not defined.
|
||||
if (expandedSections === undefined) {
|
||||
this.sections.forEach((section) => {
|
||||
section.expanded = true;
|
||||
this.accordionMultipleValue.push(section.id.toString());
|
||||
});
|
||||
this.accordionMultipleValue = [];
|
||||
|
||||
this.subSections.forEach((section) => {
|
||||
// Expand all sections if not defined.
|
||||
CoreCourseHelper.flattenSections(this.sections).forEach((section) => {
|
||||
section.expanded = true;
|
||||
this.accordionMultipleValue.push(section.id.toString());
|
||||
});
|
||||
|
@ -817,11 +799,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
this.accordionMultipleValue = expandedSections.split(',');
|
||||
|
||||
this.sections.forEach((section) => {
|
||||
section.expanded = this.accordionMultipleValue.includes(section.id.toString());
|
||||
});
|
||||
|
||||
this.subSections.forEach((section) => {
|
||||
CoreCourseHelper.flattenSections(this.sections).forEach((section) => {
|
||||
section.expanded = this.accordionMultipleValue.includes(section.id.toString());
|
||||
});
|
||||
}
|
||||
|
@ -833,21 +811,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
*/
|
||||
accordionMultipleChange(ev: AccordionGroupChangeEventDetail): void {
|
||||
const sectionIds = ev.value as string[] | undefined;
|
||||
this.sections.forEach((section) => {
|
||||
section.expanded = false;
|
||||
});
|
||||
|
||||
this.subSections.forEach((section) => {
|
||||
const allSections = CoreCourseHelper.flattenSections(this.sections);
|
||||
allSections.forEach((section) => {
|
||||
section.expanded = false;
|
||||
});
|
||||
|
||||
sectionIds?.forEach((sectionId) => {
|
||||
const sId = Number(sectionId);
|
||||
let section = this.sections.find((section) => section.id === sId);
|
||||
if (!section) {
|
||||
section = this.subSections.find((section) => section.id === sId);
|
||||
}
|
||||
|
||||
const section = allSections.find((section) => section.id === Number(sectionId));
|
||||
if (section) {
|
||||
section.expanded = true;
|
||||
}
|
||||
|
|
|
@ -56,30 +56,31 @@
|
|||
</ion-item>
|
||||
<div id="core-course-index-section-{{section.id}}" class="core-course-index-section-content">
|
||||
<ng-container *ngIf="section.expanded">
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
@if (module.subSection) {
|
||||
<ng-container *ngFor="let modOrSubsection of section.contents">
|
||||
@if (!isModule(modOrSubsection)) {
|
||||
<div class="core-course-index-subsection">
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: module.subSection}" />
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: modOrSubsection}" />
|
||||
</div>
|
||||
} @else {
|
||||
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.indented]="module.indented"
|
||||
[class.item-hightlighted]="section.highlighted" (click)="selectSectionOrModule($event, section.id, module.id)" button>
|
||||
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined" slot="start"
|
||||
aria-hidden="true" />
|
||||
<ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
|
||||
<ion-item class="module" [class.item-dimmed]="!modOrSubsection.visible" [class.indented]="modOrSubsection.indented"
|
||||
[class.item-hightlighted]="section.highlighted" (click)="selectSectionOrModule($event, section.id, modOrSubsection.id)"
|
||||
button>
|
||||
<ion-icon class="completioninfo completion_none" name="" *ngIf="modOrSubsection.completionStatus === undefined"
|
||||
slot="start" aria-hidden="true" />
|
||||
<ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="modOrSubsection.completionStatus === 0"
|
||||
slot="start" [attr.aria-label]="'core.course.todo' | translate" />
|
||||
<ion-icon class="completioninfo completion_complete" name="fas-circle"
|
||||
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
|
||||
[attr.aria-label]="'core.course.done' | translate" />
|
||||
<ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="module.completionStatus === 3" color="danger"
|
||||
slot="start" [attr.aria-label]="'core.course.failed' | translate" />
|
||||
*ngIf="modOrSubsection.completionStatus === 1 || modOrSubsection.completionStatus === 2" color="success"
|
||||
slot="start" [attr.aria-label]="'core.course.done' | translate" />
|
||||
<ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="modOrSubsection.completionStatus === 3"
|
||||
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate" />
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="module.course" />
|
||||
<core-format-text [text]="modOrSubsection.name" contextLevel="module" [contextInstanceId]="modOrSubsection.id"
|
||||
[courseId]="modOrSubsection.course" />
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
|
||||
<ion-icon name="fas-lock" *ngIf="!modOrSubsection.uservisible" slot="end" class="restricted"
|
||||
[attr.aria-label]="'core.restricted' | translate" />
|
||||
</ion-item>
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
CoreCourse,
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseProvider,
|
||||
sectionContentIsModule,
|
||||
} from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||
|
@ -43,7 +44,6 @@ import { CoreDom } from '@singletons/dom';
|
|||
export class CoreCourseCourseIndexComponent implements OnInit {
|
||||
|
||||
@Input() sections: CoreCourseSection[] = [];
|
||||
@Input() subSections: CoreCourseSection[] = [];
|
||||
@Input() selectedId?: number;
|
||||
@Input() course?: CoreCourseAnyCourseData;
|
||||
|
||||
|
@ -51,6 +51,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
|||
highlighted?: string;
|
||||
sectionsToRender: CourseIndexSection[] = [];
|
||||
loaded = false;
|
||||
isModule = sectionContentIsModule;
|
||||
|
||||
constructor(
|
||||
protected elementRef: ElementRef,
|
||||
|
@ -88,7 +89,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
|||
const enableIndentation = await CoreCourse.isCourseIndentationEnabled(site, this.course.id);
|
||||
|
||||
this.sectionsToRender = this.sections
|
||||
.filter((section) => section.component !== 'mod_subsection' && !CoreCourseHelper.isSectionStealth(section))
|
||||
.filter((section) => !CoreCourseHelper.isSectionStealth(section))
|
||||
.map((section) => this.mapSectionToRender(section, completionEnabled, enableIndentation, currentSectionData));
|
||||
|
||||
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
|
||||
|
@ -134,26 +135,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
|||
* @param moduleId Selected module id, if any.
|
||||
*/
|
||||
selectSectionOrModule(event: Event, sectionId: number, moduleId?: number): void {
|
||||
let subSectionId: number | undefined;
|
||||
this.sectionsToRender.some((section) => {
|
||||
if (section.id === sectionId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return section.modules.some((module) => {
|
||||
if (module.subSection?.id === sectionId) {
|
||||
// Always use the parent section.
|
||||
subSectionId = sectionId;
|
||||
sectionId = section.id;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
ModalController.dismiss({ event, sectionId, subSectionId, moduleId });
|
||||
ModalController.dismiss(<CoreCourseIndexSectionWithModule> { event, sectionId, moduleId });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,37 +174,26 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
|||
enableIndentation: boolean,
|
||||
currentSectionData?: CoreCourseFormatCurrentSectionData<CoreCourseSection>,
|
||||
): CourseIndexSection {
|
||||
const modules = section.modules
|
||||
.filter((module) => module.modname === 'subsection' || this.renderModule(section, module))
|
||||
.map((module) => {
|
||||
if (module.modname === 'subsection') {
|
||||
const subSectionFound = this.subSections.find((subSection) => subSection.itemid === module.instance);
|
||||
const subSection = subSectionFound
|
||||
? this.mapSectionToRender(subSectionFound, completionEnabled, enableIndentation)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: module.id,
|
||||
name: module.name,
|
||||
course: module.course,
|
||||
visible: !!module.visible,
|
||||
uservisible: CoreCourseHelper.canUserViewModule(module, section),
|
||||
indented: true,
|
||||
subSection,
|
||||
};
|
||||
const contents = section.contents
|
||||
.filter((modOrSubsection) =>
|
||||
!sectionContentIsModule(modOrSubsection) || this.renderModule(section, modOrSubsection))
|
||||
.map((modOrSubsection) => {
|
||||
if (!sectionContentIsModule(modOrSubsection)) {
|
||||
return this.mapSectionToRender(modOrSubsection, completionEnabled, enableIndentation);
|
||||
}
|
||||
|
||||
const completionStatus = completionEnabled
|
||||
? CoreCourseHelper.getCompletionStatus(module.completiondata)
|
||||
? CoreCourseHelper.getCompletionStatus(modOrSubsection.completiondata)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: module.id,
|
||||
name: module.name,
|
||||
course: module.course,
|
||||
visible: !!module.visible,
|
||||
uservisible: CoreCourseHelper.canUserViewModule(module, section),
|
||||
indented: enableIndentation && module.indent > 0,
|
||||
id: modOrSubsection.id,
|
||||
name: modOrSubsection.name,
|
||||
modname: modOrSubsection.modname,
|
||||
course: modOrSubsection.course,
|
||||
visible: !!modOrSubsection.visible,
|
||||
uservisible: CoreCourseHelper.canUserViewModule(modOrSubsection, section),
|
||||
indented: enableIndentation && modOrSubsection.indent > 0,
|
||||
completionStatus,
|
||||
};
|
||||
});
|
||||
|
@ -235,8 +206,8 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
|||
uservisible: CoreCourseHelper.canUserViewSection(section),
|
||||
expanded: section.id === this.selectedId,
|
||||
highlighted: currentSectionData?.section.id === section.id,
|
||||
hasVisibleModules: modules.length > 0,
|
||||
modules,
|
||||
hasVisibleModules: contents.length > 0,
|
||||
contents,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -251,20 +222,21 @@ type CourseIndexSection = {
|
|||
availabilityinfo: boolean;
|
||||
visible: boolean;
|
||||
uservisible: boolean;
|
||||
modules: {
|
||||
contents: (CourseIndexSection | CourseIndexModule)[];
|
||||
};
|
||||
|
||||
type CourseIndexModule = {
|
||||
id: number;
|
||||
modname: string;
|
||||
course: number;
|
||||
visible: boolean;
|
||||
indented: boolean;
|
||||
uservisible: boolean;
|
||||
completionStatus?: CoreCourseModuleCompletionStatus;
|
||||
subSection?: CourseIndexSection;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type CoreCourseIndexSectionWithModule = {
|
||||
event: Event;
|
||||
sectionId: number;
|
||||
subSectionId?: number;
|
||||
moduleId?: number;
|
||||
};
|
||||
|
|
|
@ -53,16 +53,16 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngFor="let module of modules">
|
||||
@if (module.subSection) {
|
||||
<core-course-section [course]="course" [section]="module.subSection" [lastModuleViewed]="lastModuleViewed"
|
||||
<ng-container *ngFor="let modOrSubsection of section.contents">
|
||||
@if (!isModule(modOrSubsection)) {
|
||||
<core-course-section [course]="course" [section]="modOrSubsection" [lastModuleViewed]="lastModuleViewed"
|
||||
[viewedModules]="viewedModules" [collapsible]="true" />
|
||||
} @else {
|
||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
|
||||
<core-course-module *ngIf="modOrSubsection.visibleoncoursepage !== 0" [module]="modOrSubsection" [section]="section"
|
||||
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
|
||||
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
|
||||
!viewedModules[module.id] &&
|
||||
(!module.completiondata || module.completiondata.state === completionStatusIncomplete)" />
|
||||
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === modOrSubsection.id" [class.core-course-module-not-viewed]="
|
||||
!viewedModules[modOrSubsection.id] &&
|
||||
(!modOrSubsection.completiondata || modOrSubsection.completiondata.state === completionStatusIncomplete)" />
|
||||
}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
|
|
@ -15,12 +15,9 @@ import {
|
|||
Component,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
SimpleChange,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
CoreCourseModuleData,
|
||||
CoreCourseSection,
|
||||
} from '@features/course/services/course-helper';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
|
@ -28,7 +25,7 @@ import { CoreCourseComponentsModule } from '../components.module';
|
|||
import { toBoolean } from '@/core/transforms/boolean';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
|
||||
import { CoreCourseModuleCompletionStatus } from '@features/course/services/course';
|
||||
import { CoreCourseModuleCompletionStatus, sectionContentIsModule } from '@features/course/services/course';
|
||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||
|
||||
/**
|
||||
|
@ -44,11 +41,10 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
|
|||
CoreCourseComponentsModule,
|
||||
],
|
||||
})
|
||||
export class CoreCourseSectionComponent implements OnInit, OnChanges {
|
||||
export class CoreCourseSectionComponent implements OnInit {
|
||||
|
||||
@Input({ required: true }) course!: CoreCourseAnyCourseData; // The course to render.
|
||||
@Input({ required: true }) section!: CoreCourseSectionToDisplay;
|
||||
@Input() subSections: CoreCourseSectionToDisplay[] = []; // List of subsections in the course.
|
||||
@Input({ transform: toBoolean }) collapsible = true; // Whether the section can be collapsed.
|
||||
@Input() lastModuleViewed?: CoreCourseViewedModulesDBRecord;
|
||||
@Input() viewedModules: Record<number, boolean> = {};
|
||||
|
@ -58,9 +54,9 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges {
|
|||
return this.collapsible ? 'collapsible' : 'non-collapsible';
|
||||
}
|
||||
|
||||
modules: CoreCourseModuleToDisplay[] = [];
|
||||
completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
|
||||
highlightedName?: string; // Name to highlight.
|
||||
isModule = sectionContentIsModule;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
@ -71,27 +67,7 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges {
|
|||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.section && this.section) {
|
||||
this.modules = this.section.modules;
|
||||
|
||||
this.modules.forEach((module) => {
|
||||
if (module.modname === 'subsection') {
|
||||
module.subSection = this.subSections.find((section) =>
|
||||
section.component === 'mod_subsection' && section.itemid === module.instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CoreCourseModuleToDisplay = CoreCourseModuleData & {
|
||||
subSection?: CoreCourseSectionToDisplay;
|
||||
};
|
||||
|
||||
export type CoreCourseSectionToDisplay = CoreCourseSection & {
|
||||
highlighted?: boolean;
|
||||
|
|
|
@ -42,8 +42,6 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
|
||||
nextModule?: CoreCourseModuleData;
|
||||
previousModule?: CoreCourseModuleData;
|
||||
nextModuleSection?: CoreCourseWSSection;
|
||||
previousModuleSection?: CoreCourseWSSection;
|
||||
loaded = false;
|
||||
element: HTMLElement;
|
||||
|
||||
|
@ -104,46 +102,23 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
|
||||
const sections = await CoreCourse.getSections(this.courseId, false, true, preSets);
|
||||
|
||||
// Search the next module.
|
||||
let currentModuleIndex = -1;
|
||||
|
||||
const currentSectionIndex = sections.findIndex((section) => {
|
||||
if (!this.isSectionAvailable(section)) {
|
||||
// User cannot view the section, skip it.
|
||||
return false;
|
||||
}
|
||||
|
||||
currentModuleIndex = section.modules.findIndex((module: CoreCourseModuleData) => module.id == this.currentModuleId);
|
||||
|
||||
return currentModuleIndex >= 0;
|
||||
const modules = await CoreCourse.getSectionsModules(sections, {
|
||||
ignoreSection: (section) => !this.isSectionAvailable(section),
|
||||
});
|
||||
|
||||
if (currentSectionIndex < 0) {
|
||||
// Nothing found. Return.
|
||||
|
||||
const currentModuleIndex = modules.findIndex((module) => module.id === this.currentModuleId);
|
||||
if (currentModuleIndex < 0) {
|
||||
// Current module found. Return.
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkNext) {
|
||||
// Find next Module.
|
||||
this.nextModule = undefined;
|
||||
for (let i = currentSectionIndex; i < sections.length && this.nextModule == undefined; i++) {
|
||||
const section = sections[i];
|
||||
|
||||
if (!this.isSectionAvailable(section)) {
|
||||
// User cannot view the section, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
const startModule = i == currentSectionIndex ? currentModuleIndex + 1 : 0;
|
||||
for (let j = startModule; j < section.modules.length && this.nextModule == undefined; j++) {
|
||||
const module = section.modules[j];
|
||||
|
||||
const found = await this.isModuleAvailable(module);
|
||||
if (found) {
|
||||
for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule === undefined; i++) {
|
||||
const module = modules[i];
|
||||
if (this.isModuleAvailable(module)) {
|
||||
this.nextModule = module;
|
||||
this.nextModuleSection = section;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,23 +126,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
if (checkPrevious) {
|
||||
// Find previous Module.
|
||||
this.previousModule = undefined;
|
||||
for (let i = currentSectionIndex; i >= 0 && this.previousModule == undefined; i--) {
|
||||
const section = sections[i];
|
||||
|
||||
if (!this.isSectionAvailable(section)) {
|
||||
// User cannot view the section, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
const startModule = i == currentSectionIndex ? currentModuleIndex - 1 : section.modules.length - 1;
|
||||
for (let j = startModule; j >= 0 && this.previousModule == undefined; j--) {
|
||||
const module = section.modules[j];
|
||||
|
||||
const found = await this.isModuleAvailable(module);
|
||||
if (found) {
|
||||
for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule === undefined; i--) {
|
||||
const module = modules[i];
|
||||
if (this.isModuleAvailable(module)) {
|
||||
this.previousModule = module;
|
||||
this.previousModuleSection = section;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -181,8 +143,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
* @param module Module to check.
|
||||
* @returns Wether the module is available to the user or not.
|
||||
*/
|
||||
protected async isModuleAvailable(module: CoreCourseModuleData): Promise<boolean> {
|
||||
return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.instance.moduleHasView(module);
|
||||
protected isModuleAvailable(module: CoreCourseModuleData): boolean {
|
||||
return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.moduleHasView(module);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -229,10 +191,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
if (!CoreCourseHelper.canUserViewModule(module)) {
|
||||
const section = next ? this.nextModuleSection : this.previousModuleSection;
|
||||
options.params = {
|
||||
module,
|
||||
section,
|
||||
};
|
||||
CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options);
|
||||
} else {
|
||||
|
|
|
@ -142,11 +142,7 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
const moduleSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(this.module, this.courseId);
|
||||
|
||||
if (moduleSize) {
|
||||
this.size = moduleSize;
|
||||
}
|
||||
this.size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(this.module, this.courseId);
|
||||
}, 1000);
|
||||
|
||||
this.fileStatusObserver = CoreEvents.on(
|
||||
|
|
|
@ -18,7 +18,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg
|
|||
import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||
import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleCompletionData, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import type { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
|
||||
|
@ -59,7 +59,7 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
|||
}
|
||||
|
||||
// In single activity the module should only have 1 section and 1 module. Get the module.
|
||||
const module = this.sections?.[0].modules?.[0];
|
||||
const module = this.sections?.[0].contents?.[0] as (CoreCourseModuleData | undefined);
|
||||
|
||||
this.data.courseId = this.course.id;
|
||||
this.data.module = module;
|
||||
|
|
|
@ -55,8 +55,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
|
|||
* @inheritdoc
|
||||
*/
|
||||
getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string {
|
||||
if (sections?.[0]?.modules?.[0]) {
|
||||
return sections[0].modules[0].name;
|
||||
if (sections?.[0]?.contents?.[0]) {
|
||||
return sections[0].contents[0].name;
|
||||
}
|
||||
|
||||
return course.fullname || '';
|
||||
|
@ -73,8 +73,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
|
|||
* @inheritdoc
|
||||
*/
|
||||
displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
|
||||
if (sections?.[0]?.modules?.[0]) {
|
||||
return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].modules[0].modname);
|
||||
if (sections?.[0]?.contents?.[0] && 'modname' in sections[0].contents[0]) {
|
||||
return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].contents[0].modname);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -216,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 = CoreCourse.getSectionsModules(sections);
|
||||
modules = CoreCourse.getSectionsModules(sections);
|
||||
|
||||
await CoreCourseModulePrefetchDelegate.invalidateModules(modules, this.course.id);
|
||||
}
|
||||
|
@ -228,9 +229,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
|
|||
|
||||
// Get the completion status.
|
||||
if (CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) {
|
||||
const sectionWithModules = sections.find((section) => section.modules.length > 0);
|
||||
if (!modules) {
|
||||
modules = CoreCourse.getSectionsModules(sections);
|
||||
}
|
||||
|
||||
if (sectionWithModules && sectionWithModules.modules[0].completion !== undefined) {
|
||||
if (modules[0]?.completion !== undefined) {
|
||||
// The module already has completion (3.6 onwards). Load the offline completion.
|
||||
this.modulesHaveCompletion = true;
|
||||
|
||||
|
@ -335,8 +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 = (<CoreCourseModuleData[]> [])
|
||||
.concat(...this.sections.map((section) => section.modules))
|
||||
const completionModules = CoreCourse.getSectionsModules(this.sections)
|
||||
.map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
|
||||
.reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);
|
||||
|
||||
|
|
|
@ -18,7 +18,17 @@
|
|||
|
||||
<ion-list class="core-course-module-list-wrapper">
|
||||
<ng-container *ngFor="let section of sections; index as i">
|
||||
<ion-card *ngIf="i <= lastShownSectionIndex">
|
||||
<ng-container *ngIf="i <= lastShownSectionIndex">
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}" />
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)" />
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
<ng-template #sectionTemplate let-section="section">
|
||||
<ion-card>
|
||||
<ion-item-divider class="course-section ion-text-wrap" *ngIf="section.name">
|
||||
<ion-label>
|
||||
<h2>
|
||||
|
@ -26,13 +36,13 @@
|
|||
</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngFor="let module of section.modules">
|
||||
<core-course-module [module]="module" [section]="section" [showActivityDates]="false" [showAvailability]="false"
|
||||
<ng-container *ngFor="let modOrSubsection of section.contents">
|
||||
@if (isModule(modOrSubsection)) {
|
||||
<core-course-module [module]="modOrSubsection" [section]="section" [showActivityDates]="false" [showAvailability]="false"
|
||||
[showExtra]="false" [showDownloadStatus]="false" [showCompletion]="false" [showIndentation]="false" />
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: modOrSubsection}" />
|
||||
}
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)" />
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
</ng-template>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourse, CoreCourseWSSection, sectionContentIsModule } from '@features/course/services/course';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
@ -30,6 +30,10 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
|||
@Component({
|
||||
selector: 'page-core-course-list-mod-type',
|
||||
templateUrl: 'list-mod-type.html',
|
||||
styles: `core-course-module:last-child {
|
||||
--activity-border: 0px;
|
||||
--card-padding-bottom: 0px;
|
||||
}`,
|
||||
})
|
||||
export class CoreCourseListModTypePage implements OnInit {
|
||||
|
||||
|
@ -41,6 +45,7 @@ export class CoreCourseListModTypePage implements OnInit {
|
|||
courseId = 0;
|
||||
canLoadMore = false;
|
||||
lastShownSectionIndex = -1;
|
||||
isModule = sectionContentIsModule;
|
||||
|
||||
protected modName?: string;
|
||||
protected archetypes: Record<string, number> = {}; // To speed up the check of modules.
|
||||
|
@ -97,40 +102,7 @@ export class CoreCourseListModTypePage implements OnInit {
|
|||
// Get all the modules in the course.
|
||||
let sections = await CoreCourse.getSections(this.courseId, false, true);
|
||||
|
||||
sections = sections.filter((section) => {
|
||||
if (!section.modules.length || section.hiddenbynumsections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
section.modules = section.modules.filter((mod) => {
|
||||
if (!CoreCourseHelper.canUserViewModule(mod, section) ||
|
||||
!CoreCourse.moduleHasView(mod) ||
|
||||
mod.visibleoncoursepage === 0) {
|
||||
// Ignore this module.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.modName === 'resources') {
|
||||
// Check that the module is a resource.
|
||||
if (this.archetypes[mod.modname] === undefined) {
|
||||
this.archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
|
||||
mod.modname,
|
||||
CoreConstants.FEATURE_MOD_ARCHETYPE,
|
||||
CoreConstants.MOD_ARCHETYPE_OTHER,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.archetypes[mod.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if (mod.modname === this.modName) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return section.modules.length > 0;
|
||||
});
|
||||
sections = this.filterSectionsAndContents(sections);
|
||||
|
||||
const result = await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId);
|
||||
|
||||
|
@ -143,6 +115,56 @@ export class CoreCourseListModTypePage implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of sections, return only those with contents to display. Also filter the contents to only include
|
||||
* the ones that should be displayed.
|
||||
*
|
||||
* @param sections Sections.
|
||||
* @returns Filtered sections.
|
||||
*/
|
||||
protected filterSectionsAndContents(sections: CoreCourseWSSection[]): CoreCourseWSSection[] {
|
||||
return sections.filter((section) => {
|
||||
if (!section.contents.length || section.hiddenbynumsections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
section.contents = section.contents.filter((modOrSubsection) => {
|
||||
if (!sectionContentIsModule(modOrSubsection)) {
|
||||
const formattedSections = this.filterSectionsAndContents([modOrSubsection]);
|
||||
|
||||
return !!formattedSections.length;
|
||||
}
|
||||
|
||||
if (!CoreCourseHelper.canUserViewModule(modOrSubsection, section) ||
|
||||
!CoreCourse.moduleHasView(modOrSubsection) ||
|
||||
modOrSubsection.visibleoncoursepage === 0) {
|
||||
// Ignore this module.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.modName === 'resources') {
|
||||
// Check that the module is a resource.
|
||||
if (this.archetypes[modOrSubsection.modname] === undefined) {
|
||||
this.archetypes[modOrSubsection.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
|
||||
modOrSubsection.modname,
|
||||
CoreConstants.FEATURE_MOD_ARCHETYPE,
|
||||
CoreConstants.MOD_ARCHETYPE_OTHER,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.archetypes[modOrSubsection.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if (modOrSubsection.modname === this.modName) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return section.contents.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show more activities.
|
||||
*
|
||||
|
@ -153,7 +175,8 @@ export class CoreCourseListModTypePage implements OnInit {
|
|||
while (this.lastShownSectionIndex < this.sections.length - 1 && modulesLoaded < CoreCourseListModTypePage.PAGE_LENGTH) {
|
||||
this.lastShownSectionIndex++;
|
||||
|
||||
modulesLoaded += this.sections[this.lastShownSectionIndex].modules.length;
|
||||
const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
|
||||
modulesLoaded += sectionModules.length;
|
||||
}
|
||||
|
||||
this.canLoadMore = this.lastShownSectionIndex < this.sections.length - 1;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { CoreCourseModuleSummaryResult } from '@features/course/components/module-summary/module-summary';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { CoreModals } from '@services/modals';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
@ -35,7 +35,6 @@ export class CoreCourseModulePreviewPage implements OnInit {
|
|||
|
||||
title!: string;
|
||||
module!: CoreCourseModuleData;
|
||||
section?: CoreCourseSection; // The section the module belongs to.
|
||||
courseId!: number;
|
||||
loaded = false;
|
||||
unsupported = false;
|
||||
|
@ -52,7 +51,6 @@ export class CoreCourseModulePreviewPage implements OnInit {
|
|||
try {
|
||||
this.module = CoreNavigator.getRequiredRouteParam<CoreCourseModuleData>('module');
|
||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.section = CoreNavigator.getRouteParam<CoreCourseSection>('section');
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ import {
|
|||
CoreCourseModuleCompletionTracking,
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseGetContentsWSModule,
|
||||
sectionContentIsModule,
|
||||
CoreCourseAnyModuleData,
|
||||
} from './course';
|
||||
import { CoreConstants, DownloadStatus, ContextLevel } from '@/core/constants';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
|
@ -166,11 +168,10 @@ export class CoreCourseHelperProvider {
|
|||
|
||||
let hasContent = false;
|
||||
|
||||
const formattedSections = await Promise.all(
|
||||
sections.map<Promise<CoreCourseSection>>(async (courseSection) => {
|
||||
const treatSection = async (sectionToTreat: CoreCourseWSSection): Promise<CoreCourseSection> => {
|
||||
const section = {
|
||||
...courseSection,
|
||||
hasContent: this.sectionHasContent(courseSection),
|
||||
...sectionToTreat,
|
||||
hasContent: this.sectionHasContent(sectionToTreat),
|
||||
};
|
||||
|
||||
if (!section.hasContent) {
|
||||
|
@ -179,7 +180,11 @@ export class CoreCourseHelperProvider {
|
|||
|
||||
hasContent = true;
|
||||
|
||||
await Promise.all(section.modules.map(async (module) => {
|
||||
section.contents = await Promise.all(section.contents.map(async (module) => {
|
||||
if (!sectionContentIsModule(module)) {
|
||||
return await treatSection(module);
|
||||
}
|
||||
|
||||
module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor(
|
||||
module.modname,
|
||||
module,
|
||||
|
@ -205,11 +210,14 @@ export class CoreCourseHelperProvider {
|
|||
|
||||
// Check if the module is stealth.
|
||||
module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
|
||||
|
||||
return module;
|
||||
}));
|
||||
|
||||
return section;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const formattedSections = await Promise.all(sections.map((courseSection) => treatSection(courseSection)));
|
||||
|
||||
return { hasContent, sections: formattedSections };
|
||||
}
|
||||
|
@ -218,7 +226,8 @@ export class CoreCourseHelperProvider {
|
|||
* Module is stealth.
|
||||
*
|
||||
* @param module Module to check.
|
||||
* @param section Section to check.
|
||||
* @param section Section to check. If the module belongs to a subsection, you can pass either the subsection or the parent
|
||||
* section. Subsections inherit the visibility from their parent section.
|
||||
* @returns Wether the module is stealth.
|
||||
*/
|
||||
isModuleStealth(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
|
||||
|
@ -230,7 +239,8 @@ export class CoreCourseHelperProvider {
|
|||
* Module is visible by the user.
|
||||
*
|
||||
* @param module Module to check.
|
||||
* @param section Section to check. Omitted if not defined.
|
||||
* @param section Section to check. Omitted if not defined. If the module belongs to a subsection, you can pass either the
|
||||
* subsection or the parent section. Subsections inherit the visibility from their parent section.
|
||||
* @returns Wether the section is visible by the user.
|
||||
*/
|
||||
canUserViewModule(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
|
||||
|
@ -273,15 +283,15 @@ export class CoreCourseHelperProvider {
|
|||
refresh?: boolean,
|
||||
checkUpdates: boolean = true,
|
||||
): Promise<{statusData: CoreCourseModulesStatus; section: CoreCourseSectionWithStatus}> {
|
||||
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
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 result = await CoreCourseModulePrefetchDelegate.getModulesStatus(
|
||||
section.modules,
|
||||
const statusData = await CoreCourseModulePrefetchDelegate.getModulesStatus(
|
||||
modules,
|
||||
courseId,
|
||||
section.id,
|
||||
refresh,
|
||||
|
@ -289,83 +299,35 @@ 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)) {
|
||||
result.status = DownloadStatus.DOWNLOADING;
|
||||
statusData.status = DownloadStatus.DOWNLOADING;
|
||||
}
|
||||
|
||||
sectionWithStatus.downloadStatus = result.status;
|
||||
const sectionWithStatus = <CoreCourseSectionWithStatus> section;
|
||||
sectionWithStatus.downloadStatus = statusData.status;
|
||||
|
||||
// Set this section data.
|
||||
if (result.status !== DownloadStatus.DOWNLOADING) {
|
||||
if (statusData.status !== DownloadStatus.DOWNLOADING) {
|
||||
sectionWithStatus.isDownloading = false;
|
||||
sectionWithStatus.total = 0;
|
||||
this.resetSectionDownloadCount(section);
|
||||
} else {
|
||||
// Section is being downloaded.
|
||||
sectionWithStatus.isDownloading = true;
|
||||
CoreCourseModulePrefetchDelegate.setOnProgress(downloadId, (data) => {
|
||||
sectionWithStatus.count = data.count;
|
||||
sectionWithStatus.total = data.total;
|
||||
this.setSectionDownloadCount(sectionWithStatus, data.count, data.total);
|
||||
});
|
||||
}
|
||||
|
||||
return { statusData: result, section: sectionWithStatus };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the status of a list of sections, setting attributes to determine the icons/data to be shown.
|
||||
*
|
||||
* @param sections Sections to calculate their status.
|
||||
* @param courseId Course ID the sections belong to.
|
||||
* @param refresh True if it shouldn't use module status cache (slower).
|
||||
* @param checkUpdates Whether to use the WS to check updates. Defaults to true.
|
||||
* @returns Promise resolved when the states are calculated.
|
||||
*/
|
||||
async calculateSectionsStatus(
|
||||
sections: CoreCourseSection[],
|
||||
courseId: number,
|
||||
refresh?: boolean,
|
||||
checkUpdates: boolean = true,
|
||||
): Promise<CoreCourseSectionWithStatus[]> {
|
||||
let allSectionsSection: CoreCourseSectionWithStatus | undefined;
|
||||
let allSectionsStatus = DownloadStatus.NOT_DOWNLOADABLE as DownloadStatus;
|
||||
|
||||
const promises = sections.map(async (section: CoreCourseSectionWithStatus) => {
|
||||
section.isCalculating = true;
|
||||
|
||||
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
// "All sections" section status is calculated using the status of the rest of sections.
|
||||
allSectionsSection = section;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.calculateSectionStatus(section, courseId, refresh, checkUpdates);
|
||||
|
||||
// Calculate "All sections" status.
|
||||
allSectionsStatus = CoreFilepool.determinePackagesStatus(allSectionsStatus, result.statusData.status);
|
||||
} finally {
|
||||
section.isCalculating = false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
|
||||
if (allSectionsSection) {
|
||||
// Set "All sections" data.
|
||||
allSectionsSection.downloadStatus = allSectionsStatus;
|
||||
allSectionsSection.isDownloading = allSectionsStatus === DownloadStatus.DOWNLOADING;
|
||||
}
|
||||
|
||||
return sections;
|
||||
} finally {
|
||||
if (allSectionsSection) {
|
||||
allSectionsSection.isCalculating = false;
|
||||
}
|
||||
}
|
||||
return { statusData, section: sectionWithStatus };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -401,7 +363,7 @@ export class CoreCourseHelperProvider {
|
|||
}
|
||||
|
||||
// Confirm the download.
|
||||
await this.confirmDownloadSizeSection(course.id, undefined, options.sections, true);
|
||||
await this.confirmDownloadSizeSection(course.id, options.sections, true);
|
||||
|
||||
// User confirmed, get the course handlers if needed.
|
||||
if (!options.courseHandlers) {
|
||||
|
@ -448,42 +410,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) {
|
||||
|
@ -498,48 +446,47 @@ export class CoreCourseHelperProvider {
|
|||
* Calculate the size to download a section and show a confirm modal if needed.
|
||||
*
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @param section Section. If not provided, all sections.
|
||||
* @param sections List of sections. Used when downloading all the sections.
|
||||
* @param sections List of sections to download
|
||||
* @param alwaysConfirm True to show a confirm even if the size isn't high, false otherwise.
|
||||
* @returns Promise resolved if the user confirms or there's no need to confirm.
|
||||
*/
|
||||
async confirmDownloadSizeSection(
|
||||
courseId: number,
|
||||
section?: CoreCourseWSSection,
|
||||
sections?: CoreCourseWSSection[],
|
||||
alwaysConfirm?: boolean,
|
||||
sections: CoreCourseWSSection[] = [],
|
||||
alwaysConfirm = false,
|
||||
): Promise<void> {
|
||||
let hasEmbeddedFiles = false;
|
||||
let sizeSum: CoreFileSizeSum = {
|
||||
const sizeSum: CoreFileSizeSum = {
|
||||
size: 0,
|
||||
total: true,
|
||||
};
|
||||
|
||||
// Calculate the size of the download.
|
||||
if (section && section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
sizeSum = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
|
||||
|
||||
// Check if the section has embedded files in the description.
|
||||
hasEmbeddedFiles = CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0;
|
||||
} else if (sections) {
|
||||
await Promise.all(sections.map(async (section) => {
|
||||
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
return;
|
||||
const getSectionSize = async (section: CoreCourseWSSection): Promise<CoreFileSizeSum> => {
|
||||
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
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);
|
||||
}));
|
||||
} else {
|
||||
throw new CoreError('Either section or list of sections needs to be supplied.');
|
||||
}
|
||||
|
||||
if (hasEmbeddedFiles) {
|
||||
sizeSum.total = false;
|
||||
|
@ -549,6 +496,20 @@ export class CoreCourseHelperProvider {
|
|||
await CoreDomUtils.confirmDownloadSize(sizeSum, undefined, undefined, undefined, undefined, alwaysConfirm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums the stored module sizes.
|
||||
*
|
||||
* @param modules List of modules.
|
||||
* @param courseId Course ID.
|
||||
* @returns Promise resolved with the sum of the stored sizes.
|
||||
*/
|
||||
async getModulesDownloadedSize(modules: CoreCourseAnyModuleData[], courseId: number): Promise<number> {
|
||||
const moduleSizes = await Promise.all(modules.map(async (module) =>
|
||||
await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId)));
|
||||
|
||||
return moduleSizes.reduce((totalSize, moduleSize) => totalSize + moduleSize, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a course is accessed using guest access and if it requires user input to enter.
|
||||
*
|
||||
|
@ -624,6 +585,7 @@ export class CoreCourseHelperProvider {
|
|||
summary: '',
|
||||
summaryformat: 1,
|
||||
modules: [],
|
||||
contents: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1121,31 +1083,36 @@ export class CoreCourseHelperProvider {
|
|||
const totalOffline = offlineCompletions.length;
|
||||
let loaded = 0;
|
||||
const offlineCompletionsMap = CoreUtils.arrayToObject(offlineCompletions, 'cmid');
|
||||
// Load the offline data in the modules.
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
if (!section.modules || !section.modules.length) {
|
||||
// Section has no modules, ignore it.
|
||||
|
||||
const loadSectionOfflineCompletion = (section: CoreCourseWSSection): void => {
|
||||
if (!section.contents || !section.contents.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let j = 0; j < section.contents.length && loaded < totalOffline; j++) {
|
||||
const modOrSubsection = section.contents[j];
|
||||
if (!sectionContentIsModule(modOrSubsection)) {
|
||||
loadSectionOfflineCompletion(modOrSubsection);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < section.modules.length; j++) {
|
||||
const module = section.modules[j];
|
||||
const offlineCompletion = offlineCompletionsMap[module.id];
|
||||
const offlineCompletion = offlineCompletionsMap[modOrSubsection.id];
|
||||
|
||||
if (offlineCompletion && module.completiondata !== undefined &&
|
||||
offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) {
|
||||
if (offlineCompletion && modOrSubsection.completiondata !== undefined &&
|
||||
offlineCompletion.timecompleted >= modOrSubsection.completiondata.timecompleted * 1000) {
|
||||
// The module has offline completion. Load it.
|
||||
module.completiondata.state = offlineCompletion.completed;
|
||||
module.completiondata.offline = true;
|
||||
modOrSubsection.completiondata.state = offlineCompletion.completed;
|
||||
modOrSubsection.completiondata.offline = true;
|
||||
|
||||
// If all completions have been loaded, stop.
|
||||
loaded++;
|
||||
if (loaded == totalOffline) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Load the offline data in the modules.
|
||||
for (let i = 0; i < sections.length && loaded < totalOffline; i++) {
|
||||
loadSectionOfflineCompletion(sections[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1334,20 +1301,18 @@ export class CoreCourseHelperProvider {
|
|||
await CoreUtils.ignoreErrors(CoreCourseModulePrefetchDelegate.invalidateCourseUpdates(courseId));
|
||||
}
|
||||
|
||||
const results = await Promise.all([
|
||||
const [size, status, packageData] = await Promise.all([
|
||||
CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId),
|
||||
CoreCourseModulePrefetchDelegate.getModuleStatus(module, courseId),
|
||||
this.getModulePackageLastDownloaded(module, component),
|
||||
]);
|
||||
|
||||
// Treat stored size.
|
||||
const size = results[0];
|
||||
const sizeReadable = CoreText.bytesToSize(results[0], 2);
|
||||
const sizeReadable = CoreText.bytesToSize(size, 2);
|
||||
|
||||
// Treat module status.
|
||||
const status = results[1];
|
||||
let statusIcon: string | undefined;
|
||||
switch (results[1]) {
|
||||
switch (status) {
|
||||
case DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED:
|
||||
statusIcon = CoreConstants.ICON_NOT_DOWNLOADED;
|
||||
break;
|
||||
|
@ -1364,8 +1329,6 @@ export class CoreCourseHelperProvider {
|
|||
break;
|
||||
}
|
||||
|
||||
const packageData = results[2];
|
||||
|
||||
return {
|
||||
size,
|
||||
sizeReadable,
|
||||
|
@ -1584,7 +1547,7 @@ export class CoreCourseHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved when the download finishes.
|
||||
*/
|
||||
async prefetchCourse(
|
||||
protected async prefetchCourse(
|
||||
course: CoreCourseAnyCourseData,
|
||||
sections: CoreCourseWSSection[],
|
||||
courseHandlers: CoreCourseOptionsHandlerToDisplay[],
|
||||
|
@ -1609,12 +1572,7 @@ export class CoreCourseHelperProvider {
|
|||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
// Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections".
|
||||
let allSectionsSection: CoreCourseWSSection = sections[0];
|
||||
if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
allSectionsSection = this.createAllSectionsSection();
|
||||
}
|
||||
promises.push(this.prefetchSection(allSectionsSection, course.id, sections));
|
||||
promises.push(this.prefetchSections(sections, course.id, true));
|
||||
|
||||
// Prefetch course options.
|
||||
courseHandlers.forEach((handler) => {
|
||||
|
@ -1631,8 +1589,8 @@ export class CoreCourseHelperProvider {
|
|||
// Prefetch other data needed to render the course.
|
||||
promises.push(CoreCourses.getCoursesByField('id', course.id));
|
||||
|
||||
const sectionWithModules = sections.find((section) => section.modules && section.modules.length > 0);
|
||||
if (!sectionWithModules || sectionWithModules.modules[0].completion === undefined) {
|
||||
const modules = CoreCourse.getSectionsModules(sections);
|
||||
if (!modules.length || modules[0].completion === undefined) {
|
||||
promises.push(CoreCourse.getActivitiesCompletionStatus(course.id));
|
||||
}
|
||||
|
||||
|
@ -1684,41 +1642,32 @@ export class CoreCourseHelperProvider {
|
|||
}
|
||||
|
||||
/**
|
||||
* Prefetch one section or all the sections.
|
||||
* If the section is "All sections" it will prefetch all the sections.
|
||||
* Prefetch some sections
|
||||
*
|
||||
* @param section Section.
|
||||
* @param sections List of sections. .
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @param sections List of sections. Used when downloading all the sections.
|
||||
* @returns Promise resolved when the prefetch is finished.
|
||||
* @param updateAllSections Update all sections status
|
||||
*/
|
||||
async prefetchSection(
|
||||
section: CoreCourseSectionWithStatus,
|
||||
async prefetchSections(
|
||||
sections: CoreCourseSectionWithStatus[],
|
||||
courseId: number,
|
||||
sections?: CoreCourseSectionWithStatus[],
|
||||
updateAllSections = false,
|
||||
): Promise<void> {
|
||||
if (section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
try {
|
||||
// Download only this section.
|
||||
await this.prefetchSingleSectionIfNeeded(section, courseId);
|
||||
} finally {
|
||||
// Calculate the status of the section that finished.
|
||||
await this.calculateSectionStatus(section, courseId, false, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sections) {
|
||||
throw new CoreError('List of sections is required when downloading all sections.');
|
||||
}
|
||||
|
||||
// Download all the sections except "All sections".
|
||||
let allSectionsStatus = DownloadStatus.NOT_DOWNLOADABLE as DownloadStatus;
|
||||
let allSectionsSection: (CoreCourseSectionWithStatus) | undefined;
|
||||
if (updateAllSections) {
|
||||
// Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections".
|
||||
allSectionsSection = sections[0];
|
||||
if (sections[0].id !== CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
allSectionsSection = this.createAllSectionsSection();
|
||||
}
|
||||
allSectionsSection.isDownloading = true;
|
||||
}
|
||||
|
||||
section.isDownloading = true;
|
||||
const promises = sections.map(async (section) => {
|
||||
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
// Download all the sections except "All sections".
|
||||
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1737,10 +1686,14 @@ export class CoreCourseHelperProvider {
|
|||
await CoreUtils.allPromises(promises);
|
||||
|
||||
// Set "All sections" data.
|
||||
section.downloadStatus = allSectionsStatus;
|
||||
section.isDownloading = allSectionsStatus === DownloadStatus.DOWNLOADING;
|
||||
if (allSectionsSection) {
|
||||
allSectionsSection.downloadStatus = allSectionsStatus;
|
||||
allSectionsSection.isDownloading = allSectionsStatus === DownloadStatus.DOWNLOADING;
|
||||
}
|
||||
} finally {
|
||||
section.isDownloading = false;
|
||||
if (allSectionsSection) {
|
||||
allSectionsSection.isDownloading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1753,7 +1706,7 @@ export class CoreCourseHelperProvider {
|
|||
* @returns Promise resolved when the section is prefetched.
|
||||
*/
|
||||
protected async prefetchSingleSectionIfNeeded(section: CoreCourseSectionWithStatus, courseId: number): Promise<void> {
|
||||
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID || section.hiddenbynumsections) {
|
||||
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID || section.hiddenbynumsections) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1786,11 +1739,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.modules, 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.modules, 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.
|
||||
|
@ -1798,6 +1754,18 @@ export class CoreCourseHelperProvider {
|
|||
}
|
||||
|
||||
await this.prefetchSingleSection(section, result, courseId);
|
||||
};
|
||||
|
||||
this.setSectionDownloadCount(section, 0, subsections.length, true);
|
||||
|
||||
await Promise.all([
|
||||
syncAndPrefetchModules(),
|
||||
Promise.all(subsections.map(async (subsection) => {
|
||||
await this.prefetchSingleSectionIfNeeded(subsection, courseId);
|
||||
|
||||
this.setSectionDownloadCount(section, (section.subsectionCount ?? 0) + 1, subsections.length, true);
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1814,11 +1782,11 @@ export class CoreCourseHelperProvider {
|
|||
result: CoreCourseModulesStatus,
|
||||
courseId: number,
|
||||
): Promise<void> {
|
||||
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.total && section.total > 0) {
|
||||
if (section.moduleTotal && section.moduleTotal > 0) {
|
||||
// Already being downloaded.
|
||||
return ;
|
||||
}
|
||||
|
@ -1832,8 +1800,7 @@ export class CoreCourseHelperProvider {
|
|||
|
||||
// Prefetch all modules to prevent incoeherences in download count and to download stale data not marked as outdated.
|
||||
await CoreCourseModulePrefetchDelegate.prefetchModules(downloadId, modules, courseId, (data) => {
|
||||
section.count = data.count;
|
||||
section.total = data.total;
|
||||
this.setSectionDownloadCount(section, data.count, data.total);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1844,16 +1811,12 @@ export class CoreCourseHelperProvider {
|
|||
* @returns Whether the section has content.
|
||||
*/
|
||||
sectionHasContent(section: CoreCourseWSSection): boolean {
|
||||
if (!section.modules) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (section.hiddenbynumsections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (section.availabilityinfo !== undefined && section.availabilityinfo != '') ||
|
||||
section.summary != '' || (section.modules && section.modules.length > 0);
|
||||
section.summary != '' || section.contents.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1914,7 +1877,7 @@ export class CoreCourseHelperProvider {
|
|||
async deleteCourseFiles(courseId: number): Promise<void> {
|
||||
const siteId = CoreSites.getCurrentSiteId();
|
||||
const sections = await CoreCourse.getSections(courseId);
|
||||
const modules = sections.map((section) => section.modules).flat();
|
||||
const modules = CoreCourse.getSectionsModules(sections);
|
||||
|
||||
await Promise.all([
|
||||
...modules.map((module) => this.removeModuleStoredData(module, courseId)),
|
||||
|
@ -2112,6 +2075,130 @@ export class CoreCourseHelperProvider {
|
|||
return completion.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a section by id.
|
||||
*
|
||||
* @param sections List of sections, with subsections included in the contents.
|
||||
* @param searchValue Value to search. If moduleId, returns the section that contains the module.
|
||||
* @returns Section object, list of parents (if any) from top to bottom.
|
||||
*/
|
||||
findSection<T extends CoreCourseWSSection>(
|
||||
sections: T[],
|
||||
searchValue: { id?: number; num?: number; moduleId?: number},
|
||||
): {section: T | undefined; parents: T[]} {
|
||||
if (searchValue.id === undefined && searchValue.num === undefined && searchValue.moduleId === undefined) {
|
||||
return { section: undefined, parents: [] };
|
||||
}
|
||||
|
||||
let foundSection: T | undefined;
|
||||
const parents: T[] = [];
|
||||
|
||||
const findInSection = (section: T): T | undefined => {
|
||||
if (section.id === searchValue.id || (section.section !== undefined && section.section === searchValue.num)) {
|
||||
return section;
|
||||
}
|
||||
|
||||
let foundSection: T | undefined;
|
||||
|
||||
section.contents.some(modOrSubsection => {
|
||||
if (sectionContentIsModule(modOrSubsection)) {
|
||||
if (searchValue.moduleId !== undefined && modOrSubsection.id === searchValue.moduleId) {
|
||||
foundSection = section;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
foundSection = findInSection(modOrSubsection as T);
|
||||
if (!foundSection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
parents.push(section);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return foundSection;
|
||||
};
|
||||
|
||||
sections.some(section => {
|
||||
foundSection = findInSection(section);
|
||||
|
||||
return !!foundSection;
|
||||
});
|
||||
|
||||
return { section: foundSection, parents: parents.reverse() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of sections, returns the list of sections and subsections.
|
||||
*
|
||||
* @param sections Sections.
|
||||
* @returns All sections, including subsections.
|
||||
*/
|
||||
flattenSections<T extends CoreCourseWSSection>(sections: T[]): T[] {
|
||||
const subsections: T[] = [];
|
||||
|
||||
const getSubsections = (section: T): void => {
|
||||
section.contents.forEach((modOrSubsection) => {
|
||||
if (!sectionContentIsModule(modOrSubsection)) {
|
||||
subsections.push(modOrSubsection as T);
|
||||
getSubsections(modOrSubsection as T);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
sections.forEach((section) => {
|
||||
getSubsections(section);
|
||||
});
|
||||
|
||||
return sections.concat(subsections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset download counts of a section.
|
||||
*
|
||||
* @param section Section.
|
||||
*/
|
||||
protected resetSectionDownloadCount(section: CoreCourseSectionWithStatus): void {
|
||||
section.moduleTotal = undefined;
|
||||
section.subsectionTotal = undefined;
|
||||
section.moduleCount = undefined;
|
||||
section.subsectionCount = undefined;
|
||||
section.total = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set download counts of a section.
|
||||
*
|
||||
* @param section Section.
|
||||
* @param count Count value.
|
||||
* @param total Total value.
|
||||
* @param isSubsectionCount True to set subsection count, false to set module count.
|
||||
*/
|
||||
protected setSectionDownloadCount(
|
||||
section: CoreCourseSectionWithStatus,
|
||||
count: number,
|
||||
total: number,
|
||||
isSubsectionCount = false,
|
||||
): void {
|
||||
if (isSubsectionCount) {
|
||||
section.subsectionCount = count;
|
||||
section.subsectionTotal = total;
|
||||
} else {
|
||||
section.moduleCount = count;
|
||||
section.moduleTotal = total;
|
||||
}
|
||||
|
||||
section.count = section.moduleCount !== undefined && section.subsectionCount !== undefined ?
|
||||
section.moduleCount + section.subsectionCount : undefined;
|
||||
section.total = section.moduleTotal !== undefined && section.subsectionTotal !== undefined ?
|
||||
section.moduleTotal + section.subsectionTotal : undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);
|
||||
|
@ -2119,8 +2206,9 @@ export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);
|
|||
/**
|
||||
* Section with calculated data.
|
||||
*/
|
||||
export type CoreCourseSection = CoreCourseWSSection & {
|
||||
export type CoreCourseSection = Omit<CoreCourseWSSection, 'contents'> & {
|
||||
hasContent?: boolean;
|
||||
contents: (CoreCourseModuleData | CoreCourseSection)[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -2129,8 +2217,12 @@ export type CoreCourseSection = CoreCourseWSSection & {
|
|||
export type CoreCourseSectionWithStatus = CoreCourseSection & {
|
||||
downloadStatus?: DownloadStatus; // Section status.
|
||||
isDownloading?: boolean; // Whether section is being downloaded.
|
||||
total?: number; // Total of modules being downloaded.
|
||||
count?: number; // Number of downloaded modules.
|
||||
total?: number; // Total of modules and subsections being downloaded.
|
||||
count?: number; // Number of downloaded modules and subsections.
|
||||
moduleTotal?: number; // Total of modules being downloaded.
|
||||
moduleCount?: number; // Number of downloaded modules.
|
||||
subsectionTotal?: number; // Total of subsections being downloaded.
|
||||
subsectionCount?: number; // Number of downloaded subsections.
|
||||
isCalculating?: boolean; // Whether status is being calculated.
|
||||
};
|
||||
|
||||
|
|
|
@ -62,6 +62,9 @@ import { firstValueFrom } from 'rxjs';
|
|||
import { map } from 'rxjs/operators';
|
||||
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
|
||||
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:';
|
||||
|
||||
|
@ -639,9 +642,9 @@ export class CoreCourseProvider {
|
|||
sectionId = module.section;
|
||||
}
|
||||
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
let sections: CoreCourseGetContentsWSSection[];
|
||||
try {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
// We have courseId, we can use core_course_get_contents for compatibility.
|
||||
this.logger.debug(`Getting module ${moduleId} in course ${courseId}`);
|
||||
|
||||
|
@ -657,7 +660,11 @@ export class CoreCourseProvider {
|
|||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
sections = await this.getSections(courseId, false, false, preSets, siteId);
|
||||
sections = await firstValueFrom(this.callGetSectionsWS(site, courseId, {
|
||||
excludeModules: false,
|
||||
excludeContents: false,
|
||||
preSets,
|
||||
}));
|
||||
}
|
||||
|
||||
let foundModule: CoreCourseGetContentsWSModule | undefined;
|
||||
|
@ -953,11 +960,51 @@ export class CoreCourseProvider {
|
|||
courseId: number,
|
||||
options: CoreCourseGetSectionsOptions = {},
|
||||
): WSObservable<CoreCourseWSSection[]> {
|
||||
options.includeStealthModules = options.includeStealthModules ?? true;
|
||||
|
||||
return asyncObservable(async () => {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
|
||||
return this.callGetSectionsWS(site, courseId, options).pipe(
|
||||
map(sections => {
|
||||
const siteHomeId = site.getSiteHomeId();
|
||||
let showSections = true;
|
||||
if (courseId === siteHomeId) {
|
||||
const storedNumSections = site.getStoredConfig('numsections');
|
||||
showSections = storedNumSections !== undefined && !!storedNumSections;
|
||||
}
|
||||
|
||||
if (showSections !== undefined && !showSections && sections.length > 0) {
|
||||
// Get only the last section (Main menu block section).
|
||||
sections.pop();
|
||||
}
|
||||
|
||||
// First format all the sections and their modules.
|
||||
const formattedSections: CoreCourseWSSection[] = sections.map((section) => ({
|
||||
...section,
|
||||
availabilityinfo: this.treatAvailablityInfo(section.availabilityinfo),
|
||||
modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
|
||||
contents: [],
|
||||
}));
|
||||
|
||||
// Only return the root sections, subsections are included in section contents.
|
||||
return this.addSectionsContents(formattedSections).filter((section) => !section.component);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the WS to get the course sections.
|
||||
*
|
||||
* @param site Site.
|
||||
* @param courseId The course ID.
|
||||
* @param options Options.
|
||||
* @returns Observable that returns the sections.
|
||||
*/
|
||||
protected callGetSectionsWS(
|
||||
site: CoreSite,
|
||||
courseId: number,
|
||||
options: CoreCourseGetSectionsOptions = {},
|
||||
): WSObservable<CoreCourseGetContentsWSSection[]> {
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
...options.preSets,
|
||||
cacheKey: this.getSectionsCacheKey(courseId),
|
||||
|
@ -982,33 +1029,38 @@ export class CoreCourseProvider {
|
|||
if (this.canRequestStealthModules(site)) {
|
||||
params.options.push({
|
||||
name: 'includestealthmodules',
|
||||
value: !!options.includeStealthModules,
|
||||
value: !!(options.includeStealthModules ?? true),
|
||||
});
|
||||
}
|
||||
|
||||
return site.readObservable<CoreCourseGetContentsWSSection[]>('core_course_get_contents', params, preSets).pipe(
|
||||
map(sections => {
|
||||
const siteHomeId = site.getSiteHomeId();
|
||||
let showSections = true;
|
||||
if (courseId === siteHomeId) {
|
||||
const storedNumSections = site.getStoredConfig('numsections');
|
||||
showSections = storedNumSections !== undefined && !!storedNumSections;
|
||||
return site.readObservable<CoreCourseGetContentsWSSection[]>('core_course_get_contents', params, preSets);
|
||||
}
|
||||
|
||||
if (showSections !== undefined && !showSections && sections.length > 0) {
|
||||
// Get only the last section (Main menu block section).
|
||||
sections.pop();
|
||||
/**
|
||||
* Calculate and add the section contents. Section contents include modules and subsections.
|
||||
*
|
||||
* @param sections Sections to calculate.
|
||||
* @returns Sections with contents.
|
||||
*/
|
||||
protected addSectionsContents(sections: CoreCourseWSSection[]): CoreCourseWSSection[] {
|
||||
const subsections = sections.filter((section) => !!section.component);
|
||||
const subsectionsComponents = CoreArray.unique(subsections.map(section => (section.component ?? '').replace('mod_', '')));
|
||||
|
||||
sections.forEach(section => {
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
section.contents = section.modules.map(module => {
|
||||
if (!subsectionsComponents.includes(module.modname)) {
|
||||
return module;
|
||||
}
|
||||
|
||||
// Add course to all modules.
|
||||
return sections.map((section) => ({
|
||||
...section,
|
||||
availabilityinfo: this.treatAvailablityInfo(section.availabilityinfo),
|
||||
modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
|
||||
}));
|
||||
}),
|
||||
);
|
||||
// Replace the module with the subsection. If subsection not found, the module will be removed from the list.
|
||||
const customData = CoreText.parseJSON<{ sectionid?: string | number }>(module.customdata ?? '{}', {});
|
||||
|
||||
return subsections.find(subsection => subsection.id === Number(customData.sectionid));
|
||||
}).filter((content): content is (CoreCourseWSSection | CoreCourseModuleData) => content !== undefined);
|
||||
});
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1023,16 +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.
|
||||
*/
|
||||
getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] {
|
||||
if (!sections || !sections.length) {
|
||||
return [];
|
||||
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;
|
||||
}
|
||||
|
||||
return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1587,10 +1663,47 @@ 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);
|
||||
|
||||
/**
|
||||
* Type guard to detect if a section content (module or subsection) is a module.
|
||||
*
|
||||
* @param content Section module or subsection.
|
||||
* @returns Whether section content is a module.
|
||||
*/
|
||||
export function sectionContentIsModule<Section extends CoreCourseWSSection, Module extends CoreCourseModuleData>(
|
||||
content: Module | Section,
|
||||
): content is Module {
|
||||
return 'modname' in content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common options used by modules when calling a WS through CoreSite.
|
||||
*/
|
||||
|
@ -1821,9 +1934,21 @@ export type CoreCourseGetContentsWSModule = {
|
|||
* Data returned by core_course_get_contents WS.
|
||||
*/
|
||||
export type CoreCourseWSSection = Omit<CoreCourseGetContentsWSSection, 'modules'> & {
|
||||
modules: CoreCourseModuleData[]; // List of module.
|
||||
contents: CoreCourseModuleOrSection[]; // List of modules and subsections.
|
||||
|
||||
/**
|
||||
* List of modules
|
||||
*
|
||||
* @deprecated since 4.5. Use contents instead.
|
||||
*/
|
||||
modules: CoreCourseModuleData[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Module or subsection.
|
||||
*/
|
||||
export type CoreCourseModuleOrSection = CoreCourseModuleData | CoreCourseWSSection;
|
||||
|
||||
/**
|
||||
* Params of core_course_get_course_module WS.
|
||||
*/
|
||||
|
@ -1997,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.
|
||||
};
|
||||
|
|
|
@ -472,6 +472,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCo
|
|||
* @returns Promise resolved with the total size (0 if unknown)
|
||||
*/
|
||||
async getModuleStoredSize(module: CoreCourseAnyModuleData, courseId: number): Promise<number> {
|
||||
try {
|
||||
const site = CoreSites.getCurrentSite();
|
||||
const handler = this.getPrefetchHandlerFor(module.modname);
|
||||
|
||||
|
@ -483,6 +484,9 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCo
|
|||
const totalSize = cachedSize + downloadedSize;
|
||||
|
||||
return isNaN(totalSize) ? 0 : totalSize;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
CoreFilterStateValue,
|
||||
CoreFilterAllStates,
|
||||
} from './filter';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourse, sectionContentIsModule } from '@features/course/services/course';
|
||||
import { CoreCourses } from '@features/courses/services/courses';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
||||
|
@ -180,16 +180,18 @@ export class CoreFilterHelperProvider {
|
|||
const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = [];
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (section.modules) {
|
||||
section.modules.forEach((module) => {
|
||||
if (CoreCourseHelper.canUserViewModule(module, section)) {
|
||||
section.contents.forEach((modOrSubsection) => {
|
||||
if (!sectionContentIsModule(modOrSubsection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CoreCourseHelper.canUserViewModule(modOrSubsection, section)) {
|
||||
contexts.push({
|
||||
contextlevel: ContextLevel.MODULE,
|
||||
instanceid: module.id,
|
||||
instanceid: modOrSubsection.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return contexts;
|
||||
|
|
|
@ -41,7 +41,7 @@ Feature: Main Menu opens the right page
|
|||
@lms_from4.5
|
||||
Scenario: Opens right main menu tab when defaulthomepage is set to a custom URL that belongs to a tab
|
||||
Given the following config values are set as admin:
|
||||
| defaulthomepage | #wwwroot#/message/index.php |
|
||||
| defaulthomepage | /message/index.php |
|
||||
And I entered the app as "student"
|
||||
Then "Messages" "ion-tab-button" should be selected in the app
|
||||
And I should find "Contacts" in the app
|
||||
|
@ -49,7 +49,7 @@ Feature: Main Menu opens the right page
|
|||
@lms_from4.5
|
||||
Scenario: Opens new page when defaulthomepage is set to a custom URL
|
||||
Given the following config values are set as admin:
|
||||
| defaulthomepage | #wwwroot#/badges/mybadges.php |
|
||||
| defaulthomepage | /badges/mybadges.php |
|
||||
And I entered the app as "student"
|
||||
Then I should find "Badges" in the app
|
||||
And I should find "There are currently no badges" in the app
|
||||
|
@ -61,7 +61,7 @@ Feature: Main Menu opens the right page
|
|||
@lms_from4.5
|
||||
Scenario: defaulthomepage ignored if it's set to a custom URL not supported by the app
|
||||
Given the following config values are set as admin:
|
||||
| defaulthomepage | #wwwroot#/foo/bar.php |
|
||||
| defaulthomepage | /foo/bar.php |
|
||||
And I entered the app as "student"
|
||||
Then "My courses" "ion-tab-button" should be selected in the app
|
||||
And I should find "Course 1" in the app
|
||||
|
|
|
@ -45,7 +45,9 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<core-course-module *ngFor="let module of section.modules" [module]="module" [section]="section" />
|
||||
<ng-container *ngFor="let modOrSubsection of section.contents">
|
||||
<core-course-module *ngIf="isModule(modOrSubsection)" [module]="modOrSubsection" [section]="section" />
|
||||
</ng-container>
|
||||
</section>
|
||||
|
||||
<!-- Site home items: news, categories, courses, etc. -->
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CoreSite, CoreSiteConfig } from '@classes/sites/site';
|
||||
import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course';
|
||||
import { CoreCourse, CoreCourseWSSection, sectionContentIsModule } from '@features/course/services/course';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
|
||||
|
@ -55,6 +55,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
|||
currentSite!: CoreSite;
|
||||
searchEnabled = false;
|
||||
newsForumModule?: CoreCourseModuleData;
|
||||
isModule = sectionContentIsModule;
|
||||
|
||||
protected updateSiteObserver: CoreEventObserver;
|
||||
protected logView: () => void;
|
||||
|
@ -177,9 +178,12 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
|||
|
||||
promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId));
|
||||
|
||||
if (this.section && this.section.modules) {
|
||||
if (this.section?.contents.length) {
|
||||
// Invalidate modules prefetch data.
|
||||
promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
|
||||
promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(
|
||||
CoreCourse.getSectionsModules([this.section]),
|
||||
this.siteHomeId,
|
||||
));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(async () => {
|
||||
|
|
|
@ -99,7 +99,7 @@ export class CoreSiteHomeProvider {
|
|||
throw Error('No sections found');
|
||||
}
|
||||
|
||||
const hasContent = sections.some((section) => section.summary || (section.modules && section.modules.length));
|
||||
const hasContent = sections.some((section) => section.summary || section.contents.length);
|
||||
const hasCourseBlocks = await CoreBlockHelper.hasCourseBlocks(siteHomeId);
|
||||
|
||||
if (hasContent || hasCourseBlocks) {
|
||||
|
|
|
@ -148,3 +148,15 @@ export function safeNumber(value?: unknown): SafeNumber | undefined {
|
|||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type to extract the type of each item of an array.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* type Result = ArrayElement<(A|B)[]>;
|
||||
* // ^? type Result = A|B;
|
||||
* ```
|
||||
*/
|
||||
export type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ?
|
||||
ElementType : never;
|
||||
|
|
Loading…
Reference in New Issue