MOBILE-3653 scorm: Implement index page
parent
aebc359083
commit
a2cf8db2ea
|
@ -0,0 +1,34 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { AddonModScormIndexComponent } from './index/index';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModScormIndexComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreCourseComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModScormIndexComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModScormComponentsModule {}
|
|
@ -0,0 +1,246 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
|
||||
[href]="externalUrl" iconAction="fas-external-link-alt">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
|
||||
(action)="expandDescription()" iconAction="fas-arrow-right">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
|
||||
iconAction="far-newspaper" (action)="gotoBlog()">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
|
||||
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)"
|
||||
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
|
||||
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
|
||||
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
||||
<!-- Warning message. -->
|
||||
<ion-card class="core-info-card" *ngIf="scorm && scorm.warningMessage">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
|
||||
<ion-label>{{ scorm.warningMessage }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ng-container *ngIf="scorm && loaded && !scorm.warningMessage">
|
||||
<!-- Attempts status. -->
|
||||
<ion-card *ngIf="(scorm.displayattemptstatus || offlineAttempts.length) && !skip">
|
||||
<ion-card-header class="ion-text-wrap">
|
||||
<ion-card-title>{{ 'addon.mod_scorm.attempts' | translate }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-list class="addon-mod_scorm-attempt-summary">
|
||||
<ng-container *ngIf="scorm.displayattemptstatus">
|
||||
<ion-item class="ion-text-wrap" *ngIf="scorm.maxattempt! >= 0">
|
||||
<ion-label>
|
||||
<h3>{{ 'addon.mod_scorm.noattemptsallowed' | translate }}</h3>
|
||||
</ion-label>
|
||||
<p slot="end">
|
||||
<span *ngIf="scorm.maxattempt == 0">{{ 'core.unlimited' | translate }}</span>
|
||||
<span *ngIf="scorm.maxattempt! > 0">{{ scorm.maxattempt }}</span>
|
||||
</p>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="numAttempts >= 0">
|
||||
<ion-label>
|
||||
<h3>{{ 'addon.mod_scorm.noattemptsmade' | translate }}</h3>
|
||||
</ion-label>
|
||||
<p slot="end">{{ numAttempts }}</p>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let attempt of onlineAttempts">
|
||||
<ion-label>
|
||||
<h3>{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.num}}</h3>
|
||||
</ion-label>
|
||||
<p slot="end">
|
||||
<span *ngIf="attempt.grade != -1">{{ attempt.gradeFormatted }}</span>
|
||||
<span *ngIf="attempt.grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
|
||||
</p>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let attempt of offlineAttempts">
|
||||
<ion-label>
|
||||
<h3>{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.num}}</h3>
|
||||
<p *ngIf="!scorm.maxattempt || attempt.num <= scorm.maxattempt">
|
||||
{{ 'addon.mod_scorm.offlineattemptnote' | translate }}
|
||||
</p>
|
||||
<p *ngIf="scorm.maxattempt && attempt.num > scorm.maxattempt">
|
||||
{{ 'addon.mod_scorm.offlineattemptovermax' | translate }}
|
||||
</p>
|
||||
</ion-label>
|
||||
<p slot="end">
|
||||
<span *ngIf="attempt.grade != -1">{{ attempt.gradeFormatted }}</span>
|
||||
<span *ngIf="attempt.grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
|
||||
</p>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="scorm.displayattemptstatus && gradeMethodReadable">
|
||||
<ion-label>
|
||||
<h3>{{ 'addon.mod_scorm.grademethod' | translate }}</h3>
|
||||
</ion-label>
|
||||
<p slot="end">{{ gradeMethodReadable }}</p>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="scorm.displayattemptstatus && gradeFormatted">
|
||||
<ion-label>
|
||||
<h3>{{ 'addon.mod_scorm.gradereported' | translate }}</h3>
|
||||
</ion-label>
|
||||
<p slot="end">
|
||||
<span *ngIf="grade != -1">{{ gradeFormatted }}</span>
|
||||
<span *ngIf="grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
|
||||
</p>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="syncTime">
|
||||
<ion-label>
|
||||
<h3>{{ 'core.lastsync' | translate }}</h3>
|
||||
<p>{{ syncTime }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
<!-- Synchronization warning. -->
|
||||
<ion-card class="core-warning-card" *ngIf="!errorMessage && hasOffline">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- TOC. -->
|
||||
<ion-card *ngIf="scorm && organizations && !skip &&
|
||||
((scorm.displaycoursestructure && organizations.length) || organizations.length > 1)" class="addon-mod_scorm-toc">
|
||||
<ion-card-header class="ion-text-wrap">
|
||||
<ion-card-title>{{ 'addon.mod_scorm.contents' | translate }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap" *ngIf="organizations.length > 1">
|
||||
<ion-label>{{ 'addon.mod_scorm.organizations' | translate }}</ion-label>
|
||||
<ion-select [(ngModel)]="currentOrganization.identifier" (ionChange)="loadOrganization()"
|
||||
interface="action-sheet">
|
||||
<ion-select-option *ngFor="let org of organizations" [value]="org.identifier">
|
||||
{{ org.title }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-center" *ngIf="scorm.displaycoursestructure && loadingToc">
|
||||
<ion-label>
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="scorm.displaycoursestructure && !loadingToc">
|
||||
<!-- If data shown doesn't belong to last attempt, show a warning. -->
|
||||
<ion-label>
|
||||
<p *ngIf="attemptToContinue">
|
||||
{{ 'addon.mod_scorm.dataattemptshown' | translate:{number: attemptToContinue} }}
|
||||
</p>
|
||||
<p>{{ currentOrganization.title }}</p>
|
||||
<div *ngFor="let sco of toc" class="core-padding-{{sco.level}} addon-mod_scorm-type-{{sco.scormtype}}">
|
||||
<p *ngIf="sco.isvisible">
|
||||
<ion-icon *ngIf="sco.icon" [name]="sco.icon.icon" [attr.aria-label]="sco.icon.description"
|
||||
slot="start">
|
||||
</ion-icon>
|
||||
<a *ngIf="sco.prereq && sco.launch" (click)="open($event, false, sco.id)" tappable="true">
|
||||
<core-format-text [text]="sco.title" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</a>
|
||||
<span *ngIf="!sco.prereq || !sco.launch">
|
||||
<core-format-text [text]="sco.title" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</span>
|
||||
<span *ngIf="accessInfo && accessInfo.canviewscores && sco.scoreraw">
|
||||
({{ 'addon.mod_scorm.score' | translate }}: {{sco.scoreraw}})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
<!-- Open in browser button. -->
|
||||
<ion-card *ngIf="errorMessage">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p class="text-danger">{{ errorMessage | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-button class="ion-margin ion-text-wrap" expand="block" [href]="externalUrl" core-link>
|
||||
{{ 'core.openinbrowser' | translate }}
|
||||
<ion-icon name="fas-external-link-alt" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-card>
|
||||
|
||||
<!-- Warning that user doesn't have any more attempts. -->
|
||||
<ion-card *ngIf="!errorMessage && attemptsLeft == 0">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p class="text-danger">{{ 'addon.mod_scorm.exceededmaxattempts' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Open SCORM in app form -->
|
||||
<ion-card *ngIf="!errorMessage && scorm && (!scorm.lastattemptlock || attemptsLeft > 0)">
|
||||
<ion-list>
|
||||
<ng-container *ngIf="!downloading && !skip">
|
||||
<!-- Create new attempt -->
|
||||
<ion-item class="ion-text-wrap"
|
||||
*ngIf="!scorm.forcenewattempt && numAttempts > 0 && !incomplete && attemptsLeft > 0">
|
||||
<ion-label>{{ 'addon.mod_scorm.newattempt' | translate }}</ion-label>
|
||||
<ion-checkbox slot="end" name="newAttempt" [(ngModel)]="startNewAttempt">
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="statusMessage">
|
||||
<ion-label>
|
||||
<p>{{ statusMessage | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Open mode (Preview or Normal) -->
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col *ngIf="!scorm.hidebrowse">
|
||||
<ion-button expand="block" fill="outline" (click)="open($event, true)" class="ion-text-wrap">
|
||||
{{ 'addon.mod_scorm.browse' | translate }}
|
||||
<ion-icon name="fas-search" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col>
|
||||
<ion-button expand="block" (click)="open($event)" class="ion-text-wrap">
|
||||
{{ 'addon.mod_scorm.enter' | translate }}
|
||||
<ion-icon name="fas-arrow-right" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-container>
|
||||
|
||||
<!-- Download progress. -->
|
||||
<ion-item class="ion-text-center" *ngIf="downloading">
|
||||
<ion-label>
|
||||
<ion-spinner></ion-spinner>
|
||||
<h2 *ngIf="progressMessage">{{ progressMessage | translate }}</h2>
|
||||
<core-progress-bar *ngIf="showPercentage" [progress]="percentage"></core-progress-bar>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
</core-loading>
|
|
@ -0,0 +1,19 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
.addon-mod_scorm-attempt-summary ion-item > p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.addon-mod_scorm-toc {
|
||||
// Hide all non sco icons using css to maintain padding.
|
||||
ion-icon {
|
||||
opacity: 0;
|
||||
@include margin(5px, 8px, null, null);
|
||||
}
|
||||
|
||||
.addon-mod_scorm-type-sco ion-icon {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,605 @@
|
|||
// (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 } from '@/core/constants';
|
||||
import { Component, OnInit, Optional } from '@angular/core';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { AddonModScormPrefetchHandler } from '../../services/handlers/prefetch';
|
||||
import {
|
||||
AddonModScorm,
|
||||
AddonModScormAttemptCountResult,
|
||||
AddonModScormGetScormAccessInformationWSResponse,
|
||||
AddonModScormAttemptGrade,
|
||||
AddonModScormOrganization,
|
||||
AddonModScormProvider,
|
||||
AddonModScormScorm,
|
||||
} from '../../services/scorm';
|
||||
import { AddonModScormHelper, AddonModScormTOCScoWithIcon } from '../../services/scorm-helper';
|
||||
import {
|
||||
AddonModScormAutoSyncEventData,
|
||||
AddonModScormSync,
|
||||
AddonModScormSyncProvider,
|
||||
AddonModScormSyncResult,
|
||||
} from '../../services/scorm-sync';
|
||||
|
||||
/**
|
||||
* Component that displays a SCORM entry page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-scorm-index',
|
||||
templateUrl: 'addon-mod-scorm-index.html',
|
||||
styleUrls: ['index.scss'],
|
||||
})
|
||||
export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit {
|
||||
|
||||
component = AddonModScormProvider.COMPONENT;
|
||||
moduleName = 'scorm';
|
||||
|
||||
scorm?: AddonModScormScorm; // The SCORM object.
|
||||
currentOrganization: Partial<AddonModScormOrganization> = {}; // Selected organization.
|
||||
startNewAttempt = false;
|
||||
errorMessage?: string; // Error message.
|
||||
syncTime?: string; // Last sync time.
|
||||
hasOffline = false; // Whether the SCORM has offline data.
|
||||
attemptToContinue?: number; // The attempt to continue or review.
|
||||
statusMessage?: string; // Message about the status.
|
||||
downloading = false; // Whether the SCORM is being downloaded.
|
||||
percentage?: string; // Download/unzip percentage.
|
||||
showPercentage = false; // Whether to show the percentage.
|
||||
progressMessage?: string; // Message about download/unzip.
|
||||
organizations?: AddonModScormOrganization[]; // List of organizations.
|
||||
loadingToc = false; // Whether the TOC is being loaded.
|
||||
toc?: AddonModScormTOCScoWithIcon[]; // Table of contents (structure).
|
||||
accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
|
||||
skip?: boolean; // Launch immediately.
|
||||
incomplete = false; // Whether last attempt is incomplete.
|
||||
numAttempts = -1; // Number of attempts.
|
||||
grade?: number; // Grade.
|
||||
gradeFormatted?: string; // Grade formatted.
|
||||
gradeMethodReadable?: string; // Grade method in a readable format.
|
||||
attemptsLeft = -1; // Number of attempts left.
|
||||
onlineAttempts: AttemptGrade[] = []; // Grades for online attempts.
|
||||
offlineAttempts: AttemptGrade[] = []; // Grades for offline attempts.
|
||||
|
||||
protected fetchContentDefaultError = 'addon.mod_scorm.errorgetscorm'; // Default error to show when loading contents.
|
||||
protected syncEventName = AddonModScormSyncProvider.AUTO_SYNCED;
|
||||
protected attempts?: AddonModScormAttemptCountResult; // Data about online and offline attempts.
|
||||
protected lastAttempt?: number; // Last attempt.
|
||||
protected lastIsOffline = false; // Whether the last attempt is offline.
|
||||
protected hasPlayed = false; // Whether the user has opened the player page.
|
||||
protected dataSentObserver?: CoreEventObserver; // To detect data sent to server.
|
||||
protected dataSent = false; // Whether some data was sent to server while playing the SCORM.
|
||||
|
||||
constructor(
|
||||
protected content?: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModScormIndexComponent', content, courseContentsPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
super.ngOnInit();
|
||||
|
||||
await this.loadContent(false, true);
|
||||
|
||||
if (!this.scorm) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.skip) {
|
||||
this.open();
|
||||
}
|
||||
|
||||
try {
|
||||
await AddonModScorm.logView(this.scorm.id, this.scorm.name);
|
||||
|
||||
this.checkCompletion();
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the completion.
|
||||
*/
|
||||
protected checkCompletion(): void {
|
||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a SCORM package or restores an ongoing download.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async downloadScormPackage(): Promise<void> {
|
||||
this.downloading = true;
|
||||
|
||||
try {
|
||||
await AddonModScormPrefetchHandler.download(this.module, this.courseId, undefined, (data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.percentage = undefined;
|
||||
this.showPercentage = false;
|
||||
|
||||
if (data.downloading) {
|
||||
// Downloading package.
|
||||
if (this.scorm!.packagesize && data.progress) {
|
||||
const percentageNumber = Number(data.progress.loaded / this.scorm!.packagesize) * 100;
|
||||
this.percentage = percentageNumber.toFixed(1);
|
||||
this.showPercentage = percentageNumber >= 0 && percentageNumber <= 100;
|
||||
}
|
||||
} else if (data.message) {
|
||||
// Show a message.
|
||||
this.progressMessage = data.message;
|
||||
} else if (data.progress && data.progress.loaded && data.progress.total) {
|
||||
// Unzipping package.
|
||||
const percentageNumber = Number(data.progress.loaded / data.progress.total) * 100;
|
||||
this.percentage = percentageNumber.toFixed(1);
|
||||
this.showPercentage = percentageNumber >= 0 && percentageNumber <= 100;
|
||||
}
|
||||
});
|
||||
|
||||
} finally {
|
||||
this.progressMessage = undefined;
|
||||
this.percentage = undefined;
|
||||
this.downloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||
try {
|
||||
// Get the SCORM instance.
|
||||
this.scorm = await AddonModScorm.getScorm(this.courseId, this.module.id, { moduleUrl: this.module.url });
|
||||
|
||||
this.dataRetrieved.emit(this.scorm);
|
||||
this.description = this.scorm.intro || this.description;
|
||||
this.errorMessage = AddonModScorm.isScormUnsupported(this.scorm);
|
||||
|
||||
if (this.scorm.warningMessage) {
|
||||
return; // SCORM is closed or not open yet, we can't get more data.
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
// Try to synchronize the SCORM.
|
||||
await CoreUtils.ignoreErrors(this.syncActivity(showErrors));
|
||||
}
|
||||
|
||||
const [syncTime, accessInfo] = await Promise.all([
|
||||
AddonModScormSync.getReadableSyncTime(this.scorm.id),
|
||||
AddonModScorm.getAccessInformation(this.scorm.id, { cmId: this.module.id }),
|
||||
this.fetchAttemptData(this.scorm),
|
||||
]);
|
||||
|
||||
this.syncTime = syncTime;
|
||||
this.accessInfo = accessInfo;
|
||||
|
||||
// Check whether to launch the SCORM immediately.
|
||||
if (typeof this.skip == 'undefined') {
|
||||
this.skip = !this.hasOffline && !this.errorMessage &&
|
||||
(!this.scorm.lastattemptlock || this.attemptsLeft > 0) &&
|
||||
this.accessInfo.canskipview && !this.accessInfo.canviewreport &&
|
||||
this.scorm.skipview! >= AddonModScormProvider.SKIPVIEW_FIRST &&
|
||||
(this.scorm.skipview == AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt == 0);
|
||||
}
|
||||
} finally {
|
||||
this.fillContextMenu(refresh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch attempt data.
|
||||
*
|
||||
* @param scorm Scorm.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchAttemptData(scorm: AddonModScormScorm): Promise<void> {
|
||||
// Get the number of attempts.
|
||||
this.attempts = await AddonModScorm.getAttemptCount(scorm.id, { cmId: this.module.id });
|
||||
this.hasOffline = !!this.attempts.offline.length;
|
||||
|
||||
// Determine the attempt that will be continued or reviewed.
|
||||
const attempt = await AddonModScormHelper.determineAttemptToContinue(scorm, this.attempts);
|
||||
|
||||
this.lastAttempt = attempt.num;
|
||||
this.lastIsOffline = attempt.offline;
|
||||
|
||||
if (this.lastAttempt != this.attempts.lastAttempt.num) {
|
||||
this.attemptToContinue = this.lastAttempt;
|
||||
} else {
|
||||
this.attemptToContinue = undefined;
|
||||
}
|
||||
|
||||
// Check if the last attempt is incomplete.
|
||||
this.incomplete = await AddonModScorm.isAttemptIncomplete(scorm.id, this.lastAttempt, {
|
||||
offline: this.lastIsOffline,
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
||||
this.numAttempts = this.attempts.total;
|
||||
this.gradeMethodReadable = AddonModScorm.getScormGradeMethod(scorm);
|
||||
this.attemptsLeft = AddonModScorm.countAttemptsLeft(scorm, this.attempts.lastAttempt.num);
|
||||
|
||||
if (scorm.forcenewattempt == AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS ||
|
||||
(scorm.forcenewattempt && !this.incomplete)) {
|
||||
this.startNewAttempt = true;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.getReportedGrades(scorm, this.attempts),
|
||||
this.fetchStructure(scorm),
|
||||
this.loadPackageSize(scorm),
|
||||
this.setStatusListener(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load SCORM package size if needed.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadPackageSize(scorm: AddonModScormScorm): Promise<void> {
|
||||
if (scorm.packagesize || this.errorMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// SCORM is supported but we don't have package size. Try to calculate it.
|
||||
scorm.packagesize = await AddonModScorm.calculateScormSize(scorm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the structure of the SCORM (TOC).
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchStructure(scorm: AddonModScormScorm): Promise<void> {
|
||||
this.organizations = await AddonModScorm.getOrganizations(scorm.id, { cmId: this.module.id });
|
||||
|
||||
if (!this.currentOrganization.identifier) {
|
||||
// Load first organization (if any).
|
||||
if (this.organizations.length) {
|
||||
this.currentOrganization.identifier = this.organizations[0].identifier;
|
||||
} else {
|
||||
this.currentOrganization.identifier = '';
|
||||
}
|
||||
}
|
||||
|
||||
return this.loadOrganizationToc(scorm, this.currentOrganization.identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grade of an attempt and add it to the scorm attempts list.
|
||||
*
|
||||
* @param attempt The attempt number.
|
||||
* @param offline Whether it's an offline attempt.
|
||||
* @param attempts Object where to add the attempt.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async getAttemptGrade(
|
||||
attempt: number,
|
||||
offline: boolean,
|
||||
attempts: Record<number, AddonModScormAttemptGrade>,
|
||||
): Promise<void> {
|
||||
const grade = await AddonModScorm.getAttemptGrade(this.scorm!, attempt, offline);
|
||||
|
||||
attempts[attempt] = {
|
||||
num: attempt,
|
||||
grade: grade,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grades of each attempt and the grade of the SCORM.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async getReportedGrades(scorm: AddonModScormScorm, attempts: AddonModScormAttemptCountResult): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
const onlineAttempts: Record<number, AttemptGrade> = {};
|
||||
const offlineAttempts: Record<number, AttemptGrade> = {};
|
||||
|
||||
// Calculate the grade for each attempt.
|
||||
attempts.online.forEach((attempt) => {
|
||||
// Check that attempt isn't in offline to prevent showing the same attempt twice. Offline should be more recent.
|
||||
if (attempts.offline.indexOf(attempt) == -1) {
|
||||
promises.push(this.getAttemptGrade(attempt, false, onlineAttempts));
|
||||
}
|
||||
});
|
||||
|
||||
attempts.offline.forEach((attempt) => {
|
||||
promises.push(this.getAttemptGrade(attempt, true, offlineAttempts));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Calculate the grade of the whole SCORM. We only use online attempts to calculate this data.
|
||||
this.grade = AddonModScorm.calculateScormGrade(scorm, onlineAttempts);
|
||||
|
||||
// Add the attempts to the SCORM in array format in ASC order, and format the grades.
|
||||
this.onlineAttempts = CoreUtils.objectToArray(onlineAttempts);
|
||||
this.offlineAttempts = CoreUtils.objectToArray(offlineAttempts);
|
||||
this.onlineAttempts.sort((a, b) => a.num - b.num);
|
||||
this.offlineAttempts.sort((a, b) => a.num - b.num);
|
||||
|
||||
// Now format the grades.
|
||||
this.onlineAttempts.forEach((attempt) => {
|
||||
attempt.gradeFormatted = AddonModScorm.formatGrade(scorm, attempt.grade);
|
||||
});
|
||||
this.offlineAttempts.forEach((attempt) => {
|
||||
attempt.gradeFormatted = AddonModScorm.formatGrade(scorm, attempt.grade);
|
||||
});
|
||||
|
||||
this.gradeFormatted = AddonModScorm.formatGrade(scorm, this.grade);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @return If suceed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModScormSyncResult): boolean {
|
||||
if (result.updated || this.dataSent) {
|
||||
// Check completion status if something was sent.
|
||||
this.checkCompletion();
|
||||
}
|
||||
|
||||
this.dataSent = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
super.ionViewDidEnter();
|
||||
|
||||
if (!this.hasPlayed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasPlayed = false;
|
||||
this.startNewAttempt = false; // Uncheck new attempt.
|
||||
|
||||
// Add a delay to make sure the player has started the last writing calls so we can detect conflicts.
|
||||
setTimeout(() => {
|
||||
this.dataSentObserver?.off(); // Stop listening for changes.
|
||||
this.dataSentObserver = undefined;
|
||||
|
||||
// Refresh data.
|
||||
this.showLoadingAndRefresh(true, false);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModScorm.invalidateScormData(this.courseId));
|
||||
|
||||
if (this.scorm) {
|
||||
promises.push(AddonModScorm.invalidateAllScormData(this.scorm.id));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares sync event data with current data to check if refresh content is needed.
|
||||
*
|
||||
* @param syncEventData Data receiven on sync observer.
|
||||
* @return True if refresh is needed, false otherwise.
|
||||
*/
|
||||
protected isRefreshSyncNeeded(syncEventData: AddonModScormAutoSyncEventData): boolean {
|
||||
if (syncEventData.updated && this.scorm && syncEventData.scormId == this.scorm.id) {
|
||||
// Check completion status.
|
||||
this.checkCompletion();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a organization's TOC.
|
||||
*/
|
||||
async loadOrganization(): Promise<void> {
|
||||
try {
|
||||
await this.loadOrganizationToc(this.scorm!, this.currentOrganization.identifier!);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the TOC of a certain organization.
|
||||
*
|
||||
* @param organizationId The organization id.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadOrganizationToc(scorm: AddonModScormScorm, organizationId: string): Promise<void> {
|
||||
if (!scorm.displaycoursestructure) {
|
||||
// TOC is not displayed, no need to load it.
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingToc = true;
|
||||
|
||||
try {
|
||||
this.toc = await AddonModScormHelper.getToc(scorm.id, this.lastAttempt!, this.incomplete, {
|
||||
organization: organizationId,
|
||||
offline: this.lastIsOffline,
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
||||
// Search organization title.
|
||||
this.organizations!.forEach((org) => {
|
||||
if (org.identifier == organizationId) {
|
||||
this.currentOrganization.title = org.title;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.loadingToc = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a SCORM. It will download the SCORM package if it's not downloaded or it has changed.
|
||||
*
|
||||
* @param event Event.
|
||||
* @param scoId SCO that needs to be loaded when the SCORM is opened. If not defined, load first SCO.
|
||||
*/
|
||||
async open(event?: Event, preview: boolean = false, scoId?: number): Promise<void> {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (this.downloading) {
|
||||
// Scope is being downloaded, abort.
|
||||
return;
|
||||
}
|
||||
|
||||
const isOutdated = this.currentStatus == CoreConstants.OUTDATED;
|
||||
const scorm = this.scorm!;
|
||||
|
||||
if (!isOutdated && this.currentStatus != CoreConstants.NOT_DOWNLOADED) {
|
||||
// Already downloaded, open it.
|
||||
this.openScorm(scoId, preview);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// SCORM needs to be downloaded.
|
||||
await AddonModScormHelper.confirmDownload(scorm, isOutdated);
|
||||
// Invalidate WS data if SCORM is outdated.
|
||||
if (isOutdated) {
|
||||
await CoreUtils.ignoreErrors(AddonModScorm.invalidateAllScormData(scorm.id));
|
||||
}
|
||||
|
||||
try {
|
||||
await this.downloadScormPackage();
|
||||
// Success downloading, open SCORM if user hasn't left the view.
|
||||
if (!this.isDestroyed) {
|
||||
this.openScorm(scoId, preview);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.showErrorModalDefault(
|
||||
error,
|
||||
Translate.instant('addon.mod_scorm.errordownloadscorm', { name: scorm.name }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a SCORM package.
|
||||
*
|
||||
* @param scoId SCO ID.
|
||||
*/
|
||||
protected openScorm(scoId?: number, preview: boolean = false): void {
|
||||
// Display the full page when returning to the page.
|
||||
this.skip = false;
|
||||
this.hasPlayed = true;
|
||||
|
||||
// Detect if anything was sent to server.
|
||||
this.dataSentObserver?.off();
|
||||
|
||||
this.dataSentObserver = CoreEvents.on(AddonModScormProvider.DATA_SENT_EVENT, (data) => {
|
||||
if (data.scormId === this.scorm!.id) {
|
||||
this.dataSent = true;
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
CoreNavigator.navigate('player', {
|
||||
params: {
|
||||
mode: preview ? AddonModScormProvider.MODEBROWSE : AddonModScormProvider.MODENORMAL,
|
||||
moduleUrl: this.module.url,
|
||||
newAttempt: !!this.startNewAttempt,
|
||||
organizationId: this.currentOrganization.identifier,
|
||||
scoId: scoId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async showStatus(status: string): Promise<void> {
|
||||
|
||||
if (status == CoreConstants.OUTDATED && this.scorm) {
|
||||
// Only show the outdated message if the file should be downloaded.
|
||||
const download = await AddonModScorm.shouldDownloadMainFile(this.scorm, true);
|
||||
|
||||
this.statusMessage = download ? 'addon.mod_scorm.scormstatusoutdated' : '';
|
||||
} else if (status == CoreConstants.NOT_DOWNLOADED) {
|
||||
this.statusMessage = 'addon.mod_scorm.scormstatusnotdownloaded';
|
||||
} else if (status == CoreConstants.DOWNLOADING) {
|
||||
if (!this.downloading) {
|
||||
// It's being downloaded right now but the view isn't tracking it. "Restore" the download.
|
||||
this.downloadScormPackage();
|
||||
}
|
||||
} else {
|
||||
this.statusMessage = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async sync(): Promise<AddonModScormSyncResult> {
|
||||
const result = await AddonModScormSync.syncScorm(this.scorm!);
|
||||
|
||||
if (!result.updated && this.dataSent) {
|
||||
// The user sent data to server, but not in the sync process. Check if we need to fetch data.
|
||||
await CoreUtils.ignoreErrors(
|
||||
AddonModScormSync.prefetchAfterUpdate(AddonModScormPrefetchHandler.instance, this.module, this.courseId),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Grade for an online attempt.
|
||||
*/
|
||||
export type AttemptGrade = AddonModScormAttemptGrade & {
|
||||
gradeFormatted?: string;
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<addon-mod-scorm-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)">
|
||||
</addon-mod-scorm-index>
|
||||
</ion-content>
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
|
||||
import { AddonModScormIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays the scorm entry page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-scorm-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModScormIndexPage extends CoreCourseModuleMainActivityPage<AddonModScormIndexComponent> implements OnInit {
|
||||
|
||||
@ViewChild(AddonModScormIndexComponent) activityComponent?: AddonModScormIndexComponent;
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonModScormComponentsModule } from './components/components.module';
|
||||
import { AddonModScormIndexPage } from './pages/index/index';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModScormIndexPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
AddonModScormComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonModScormIndexPage,
|
||||
],
|
||||
})
|
||||
export class AddonModScormLazyModule {}
|
|
@ -13,25 +13,44 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreCronDelegate } from '@services/cron';
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
import { AddonModScormComponentsModule } from './components/components.module';
|
||||
import { OFFLINE_SITE_SCHEMA } from './services/database/scorm';
|
||||
import { AddonModScormGradeLinkHandler } from './services/handlers/grade-link';
|
||||
import { AddonModScormIndexLinkHandler } from './services/handlers/index-link';
|
||||
import { AddonModScormListLinkHandler } from './services/handlers/list-link';
|
||||
import { AddonModScormModuleHandler } from './services/handlers/module';
|
||||
import { AddonModScormModuleHandler, AddonModScormModuleHandlerService } from './services/handlers/module';
|
||||
import { AddonModScormPrefetchHandler } from './services/handlers/prefetch';
|
||||
import { AddonModScormSyncCronHandler } from './services/handlers/sync-cron';
|
||||
import { AddonModScormProvider } from './services/scorm';
|
||||
import { AddonModScormHelperProvider } from './services/scorm-helper';
|
||||
import { AddonModScormOfflineProvider } from './services/scorm-offline';
|
||||
import { AddonModScormSyncProvider } from './services/scorm-sync';
|
||||
|
||||
export const ADDON_MOD_SCORM_SERVICES: Type<unknown>[] = [
|
||||
AddonModScormProvider,
|
||||
AddonModScormOfflineProvider,
|
||||
AddonModScormHelperProvider,
|
||||
AddonModScormSyncProvider,
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: AddonModScormModuleHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('./scorm-lazy.module').then(m => m.AddonModScormLazyModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
AddonModScormComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
@ -480,3 +480,10 @@ ion-button.core-button-select {
|
|||
.core-browser-copy-area {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Different levels of padding.
|
||||
@for $i from 0 through 15 {
|
||||
.core-padding-#{$i} {
|
||||
@include padding(null, null, null, 15px * $i + 16px);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue