MOBILE-4669 scorm: Use online player for unsupported scorms
parent
0bd461799e
commit
bab16335d0
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<!-- Activity info. -->
|
<!-- Activity info. -->
|
||||||
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
|
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
|
||||||
[courseId]="courseId" [hasDataToSync]="!errorMessage && hasOffline" (completionChanged)="onCompletionChange()" />
|
[courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()" />
|
||||||
|
|
||||||
<!-- Warning message. -->
|
<!-- Warning message. -->
|
||||||
<ion-card class="core-info-card" *ngIf="scorm && scorm.warningMessage">
|
<ion-card class="core-info-card" *ngIf="scorm && scorm.warningMessage">
|
||||||
|
@ -162,21 +162,8 @@
|
||||||
|
|
||||||
<div collapsible-footer *ngIf="!showLoading" slot="fixed">
|
<div collapsible-footer *ngIf="!showLoading" slot="fixed">
|
||||||
<div class="list-item-limited-width" *ngIf="scorm && !scorm.warningMessage">
|
<div class="list-item-limited-width" *ngIf="scorm && !scorm.warningMessage">
|
||||||
<!-- Open in browser button. -->
|
|
||||||
<ng-container *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]="module.url" core-link [showBrowserWarning]="false">
|
|
||||||
{{ 'core.openinbrowser' | translate }}
|
|
||||||
<ion-icon name="fas-up-right-from-square" slot="end" aria-hidden="true" />
|
|
||||||
</ion-button>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Warning that user doesn't have any more attempts. -->
|
<!-- Warning that user doesn't have any more attempts. -->
|
||||||
<ion-card *ngIf="!errorMessage && attemptsLeft === 0" class="core-danger-card">
|
<ion-card *ngIf="attemptsLeft === 0" class="core-danger-card">
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p>{{ 'addon.mod_scorm.exceededmaxattempts' | translate }}</p>
|
<p>{{ 'addon.mod_scorm.exceededmaxattempts' | translate }}</p>
|
||||||
|
@ -184,7 +171,16 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<ng-container *ngIf="!errorMessage && (!scorm.lastattemptlock || attemptsLeft > 0)">
|
<ion-card *ngIf="useOnlinePlayer && !isOnline" class="core-warning-card">
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
|
||||||
|
<ion-label>
|
||||||
|
{{ 'core.course.activitynotavailableoffline' | translate }} {{ 'core.needinternettoaccessit' | translate }}
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ng-container *ngIf="(!scorm.lastattemptlock || attemptsLeft > 0) && (!useOnlinePlayer || isOnline)">
|
||||||
<!-- Open SCORM in app form -->
|
<!-- Open SCORM in app form -->
|
||||||
<ng-container *ngIf="!downloading && !skip">
|
<ng-container *ngIf="!downloading && !skip">
|
||||||
<!-- Create new attempt -->
|
<!-- Create new attempt -->
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { DownloadStatus } from '@/core/constants';
|
import { DownloadStatus } from '@/core/constants';
|
||||||
import { Component, Input, OnInit, Optional } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
|
@ -23,7 +23,7 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSync } from '@services/sync';
|
import { CoreSync } from '@services/sync';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreObject } from '@singletons/object';
|
import { CoreObject } from '@singletons/object';
|
||||||
import { Translate } from '@singletons';
|
import { NgZone, Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { AddonModScormPrefetchHandler } from '../../services/handlers/prefetch';
|
import { AddonModScormPrefetchHandler } from '../../services/handlers/prefetch';
|
||||||
import {
|
import {
|
||||||
|
@ -51,6 +51,8 @@ import {
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { CoreWait } from '@singletons/wait';
|
import { CoreWait } from '@singletons/wait';
|
||||||
import { CorePromiseUtils } from '@singletons/promise-utils';
|
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||||
|
import { CoreNetwork } from '@services/network';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a SCORM entry page.
|
* Component that displays a SCORM entry page.
|
||||||
|
@ -60,7 +62,7 @@ import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||||
templateUrl: 'addon-mod-scorm-index.html',
|
templateUrl: 'addon-mod-scorm-index.html',
|
||||||
styleUrl: 'index.scss',
|
styleUrl: 'index.scss',
|
||||||
})
|
})
|
||||||
export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit {
|
export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@Input() autoPlayData?: AddonModScormAutoPlayData; // Data to use to play the SCORM automatically.
|
@Input() autoPlayData?: AddonModScormAutoPlayData; // Data to use to play the SCORM automatically.
|
||||||
|
|
||||||
|
@ -73,7 +75,6 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
}; // Selected organization.
|
}; // Selected organization.
|
||||||
|
|
||||||
startNewAttempt = false;
|
startNewAttempt = false;
|
||||||
errorMessage?: string; // Error message.
|
|
||||||
syncTime?: string; // Last sync time.
|
syncTime?: string; // Last sync time.
|
||||||
hasOffline = false; // Whether the SCORM has offline data.
|
hasOffline = false; // Whether the SCORM has offline data.
|
||||||
attemptToContinue?: number; // The attempt to continue or review.
|
attemptToContinue?: number; // The attempt to continue or review.
|
||||||
|
@ -96,6 +97,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
onlineAttempts: AttemptGrade[] = []; // Grades for online attempts.
|
onlineAttempts: AttemptGrade[] = []; // Grades for online attempts.
|
||||||
offlineAttempts: AttemptGrade[] = []; // Grades for offline attempts.
|
offlineAttempts: AttemptGrade[] = []; // Grades for offline attempts.
|
||||||
gradesExpanded = false;
|
gradesExpanded = false;
|
||||||
|
isOnline: boolean;
|
||||||
|
|
||||||
protected fetchContentDefaultError = 'addon.mod_scorm.errorgetscorm'; // Default error to show when loading contents.
|
protected fetchContentDefaultError = 'addon.mod_scorm.errorgetscorm'; // Default error to show when loading contents.
|
||||||
protected syncEventName = ADDON_MOD_SCORM_DATA_AUTO_SYNCED;
|
protected syncEventName = ADDON_MOD_SCORM_DATA_AUTO_SYNCED;
|
||||||
|
@ -105,12 +107,22 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
protected hasPlayed = false; // Whether the user has opened the player page.
|
protected hasPlayed = false; // Whether the user has opened the player page.
|
||||||
protected dataSentObserver?: CoreEventObserver; // To detect data sent to server.
|
protected dataSentObserver?: CoreEventObserver; // To detect data sent to server.
|
||||||
protected dataSent = false; // Whether some data was sent to server while playing the SCORM.
|
protected dataSent = false; // Whether some data was sent to server while playing the SCORM.
|
||||||
|
protected useOnlinePlayer = false; // Whether the SCORM needs to be played using an online player.
|
||||||
|
protected onlineObserver: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected content?: IonContent,
|
protected content?: IonContent,
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModScormIndexComponent', content, courseContentsPage);
|
super('AddonModScormIndexComponent', content, courseContentsPage);
|
||||||
|
|
||||||
|
this.isOnline = CoreNetwork.isOnline();
|
||||||
|
this.onlineObserver = CoreNetwork.onChange().subscribe(() => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.run(() => {
|
||||||
|
this.isOnline = CoreNetwork.isOnline();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -181,19 +193,19 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
this.dataRetrieved.emit(this.scorm);
|
this.dataRetrieved.emit(this.scorm);
|
||||||
this.description = this.scorm.intro || this.description;
|
this.description = this.scorm.intro || this.description;
|
||||||
this.errorMessage = AddonModScorm.isScormUnsupported(this.scorm);
|
this.useOnlinePlayer = AddonModScorm.useOnlinePlayer(this.scorm);
|
||||||
|
|
||||||
if (this.scorm.warningMessage) {
|
if (this.scorm.warningMessage) {
|
||||||
return; // SCORM is closed or not open yet, we can't get more data.
|
return; // SCORM is closed or not open yet, we can't get more data.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sync) {
|
if (sync && !this.useOnlinePlayer) {
|
||||||
// Try to synchronize the SCORM.
|
// Try to synchronize the SCORM.
|
||||||
await CorePromiseUtils.ignoreErrors(this.syncActivity(showErrors));
|
await CorePromiseUtils.ignoreErrors(this.syncActivity(showErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
const [syncTime, accessInfo] = await Promise.all([
|
const [syncTime, accessInfo] = await Promise.all([
|
||||||
AddonModScormSync.getReadableSyncTime(this.scorm.id),
|
this.useOnlinePlayer ? undefined : AddonModScormSync.getReadableSyncTime(this.scorm.id),
|
||||||
AddonModScorm.getAccessInformation(this.scorm.id, { cmId: this.module.id }),
|
AddonModScorm.getAccessInformation(this.scorm.id, { cmId: this.module.id }),
|
||||||
this.fetchAttemptData(this.scorm),
|
this.fetchAttemptData(this.scorm),
|
||||||
]);
|
]);
|
||||||
|
@ -203,7 +215,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
// Check whether to launch the SCORM immediately.
|
// Check whether to launch the SCORM immediately.
|
||||||
if (this.skip === undefined) {
|
if (this.skip === undefined) {
|
||||||
this.skip = !this.hasOffline && !this.errorMessage && (!this.scorm.lastattemptlock || this.attemptsLeft > 0) &&
|
this.skip = !this.hasOffline && (!this.scorm.lastattemptlock || this.attemptsLeft > 0) &&
|
||||||
(
|
(
|
||||||
!!this.autoPlayData
|
!!this.autoPlayData
|
||||||
||
|
||
|
||||||
|
@ -268,7 +280,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadPackageSize(scorm: AddonModScormScorm): Promise<void> {
|
protected async loadPackageSize(scorm: AddonModScormScorm): Promise<void> {
|
||||||
if (scorm.packagesize || this.errorMessage) {
|
if (scorm.packagesize || this.useOnlinePlayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,6 +510,15 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
|
|
||||||
|
if (this.useOnlinePlayer) {
|
||||||
|
// No need to download the package, just open it.
|
||||||
|
if (this.isOnline) {
|
||||||
|
this.openScorm(scoId, preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.downloading || !this.scorm) {
|
if (this.downloading || !this.scorm) {
|
||||||
// Scope is being downloaded, abort.
|
// Scope is being downloaded, abort.
|
||||||
return;
|
return;
|
||||||
|
@ -569,8 +590,10 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
}
|
}
|
||||||
}, this.siteId);
|
}, this.siteId);
|
||||||
|
|
||||||
|
const pageRoute = this.useOnlinePlayer ? 'online-player' : 'player';
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(
|
CoreNavigator.navigateToSitePath(
|
||||||
`${ADDON_MOD_SCORM_PAGE_NAME}/${this.courseId}/${this.module.id}/player`,
|
`${ADDON_MOD_SCORM_PAGE_NAME}/${this.courseId}/${this.module.id}/${pageRoute}`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
mode: autoPlayData?.mode ?? (preview ? AddonModScormMode.BROWSE : AddonModScormMode.NORMAL),
|
mode: autoPlayData?.mode ?? (preview ? AddonModScormMode.BROWSE : AddonModScormMode.NORMAL),
|
||||||
|
@ -587,6 +610,11 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async showStatus(status: DownloadStatus): Promise<void> {
|
protected async showStatus(status: DownloadStatus): Promise<void> {
|
||||||
|
if (this.useOnlinePlayer) {
|
||||||
|
this.statusMessage = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (status === DownloadStatus.OUTDATED && this.scorm) {
|
if (status === DownloadStatus.OUTDATED && this.scorm) {
|
||||||
// Only show the outdated message if the file should be downloaded.
|
// Only show the outdated message if the file should be downloaded.
|
||||||
|
@ -635,6 +663,13 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.onlineObserver.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,10 +14,7 @@
|
||||||
"errorcreateofflineattempt": "An error occurred while creating a new offline attempt. Please try again.",
|
"errorcreateofflineattempt": "An error occurred while creating a new offline attempt. Please try again.",
|
||||||
"errordownloadscorm": "Error downloading SCORM: \"{{name}}\".",
|
"errordownloadscorm": "Error downloading SCORM: \"{{name}}\".",
|
||||||
"errorgetscorm": "Error getting SCORM data.",
|
"errorgetscorm": "Error getting SCORM data.",
|
||||||
"errorinvalidversion": "Sorry, the application only supports SCORM 1.2.",
|
|
||||||
"errornotdownloadable": "Your school or learning provider has disabled the download of SCORM packages.",
|
|
||||||
"errornovalidsco": "This SCORM package doesn't have a visible SCO to load.",
|
"errornovalidsco": "This SCORM package doesn't have a visible SCO to load.",
|
||||||
"errorpackagefile": "Sorry, the application only supports ZIP packages.",
|
|
||||||
"errorsyncscorm": "An error occurred while synchronising. Please try again.",
|
"errorsyncscorm": "An error occurred while synchronising. Please try again.",
|
||||||
"exceededmaxattempts": "You have reached the maximum number of attempts.",
|
"exceededmaxattempts": "You have reached the maximum number of attempts.",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [text]="'core.back' | translate" />
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>
|
||||||
|
<h1>
|
||||||
|
<core-format-text *ngIf="scorm" [text]="scorm.name" contextLevel="module" [contextInstanceId]="cmId"
|
||||||
|
[courseId]="courseId" />
|
||||||
|
</h1>
|
||||||
|
</ion-title>
|
||||||
|
<!-- Add empty ion-buttons to let iframe add the full screen button -->
|
||||||
|
<ion-buttons slot="end" />
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<core-iframe *ngIf="loaded" id="scorm_object" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"
|
||||||
|
[showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true" />
|
||||||
|
|
||||||
|
<p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,278 @@
|
||||||
|
// (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, OnDestroy } from '@angular/core';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreSitesReadingStrategy } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
import {
|
||||||
|
AddonModScorm,
|
||||||
|
AddonModScormAttemptCountResult,
|
||||||
|
AddonModScormGetScormAccessInformationWSResponse,
|
||||||
|
AddonModScormScorm,
|
||||||
|
AddonModScormScoWithData,
|
||||||
|
} from '../../services/scorm';
|
||||||
|
import { AddonModScormHelper } from '../../services/scorm-helper';
|
||||||
|
import { AddonModScormMode } from '../../constants';
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { CoreNetwork } from '@services/network';
|
||||||
|
import { NgZone, Translate } from '@singletons';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that allows playing a SCORM in online, served from the server.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-scorm-online-player',
|
||||||
|
templateUrl: 'online-player.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
scorm!: AddonModScormScorm; // The SCORM object.
|
||||||
|
loaded = false; // Whether the data has been loaded.
|
||||||
|
src?: string; // Iframe src.
|
||||||
|
errorMessage?: string; // Error message.
|
||||||
|
scormWidth?: number; // Width applied to scorm iframe.
|
||||||
|
scormHeight?: number; // Height applied to scorm iframe.
|
||||||
|
cmId!: number; // Course module ID.
|
||||||
|
courseId!: number; // Course ID.
|
||||||
|
|
||||||
|
protected mode!: AddonModScormMode; // Mode to play the SCORM.
|
||||||
|
protected moduleUrl!: string; // Module URL.
|
||||||
|
protected newAttempt = false; // Whether to start a new attempt.
|
||||||
|
protected organizationId?: string; // Organization ID to load.
|
||||||
|
protected attempt = 0; // The attempt number.
|
||||||
|
protected initialScoId?: number; // Initial SCO ID to load.
|
||||||
|
protected onlineObserver: Subscription;
|
||||||
|
protected isDestroyed = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
let isOnline = CoreNetwork.isOnline();
|
||||||
|
this.onlineObserver = CoreNetwork.onChange().subscribe(() => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.run(() => {
|
||||||
|
const wasOnline = isOnline;
|
||||||
|
isOnline = CoreNetwork.isOnline();
|
||||||
|
|
||||||
|
if (!isOnline && wasOnline) {
|
||||||
|
// User lost connection while playing an online package. Show an error.
|
||||||
|
CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'))); // , {
|
||||||
|
// title: Translate.instant('core.youreoffline'),
|
||||||
|
// }));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
|
this.mode = CoreNavigator.getRouteParam('mode') || AddonModScormMode.NORMAL;
|
||||||
|
this.moduleUrl = CoreNavigator.getRouteParam('moduleUrl') || '';
|
||||||
|
this.newAttempt = !!CoreNavigator.getRouteBooleanParam('newAttempt');
|
||||||
|
this.organizationId = CoreNavigator.getRouteParam('organizationId');
|
||||||
|
this.initialScoId = CoreNavigator.getRouteNumberParam('scoId');
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the SCORM data.
|
||||||
|
await this.fetchData();
|
||||||
|
|
||||||
|
if (!this.src) {
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize.
|
||||||
|
*/
|
||||||
|
protected async initialize(): Promise<void> {
|
||||||
|
// Get the SCORM instance.
|
||||||
|
this.scorm = await AddonModScorm.getScorm(this.courseId, this.cmId, {
|
||||||
|
moduleUrl: this.moduleUrl,
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.scorm.popup || !this.scorm.width || this.scorm.width <= 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we receive a value > 100 we assume it's a fixed pixel size.
|
||||||
|
this.scormWidth = this.scorm.width;
|
||||||
|
|
||||||
|
// Only get fixed size on height if width is also fixed.
|
||||||
|
if (this.scorm.height && this.scorm.height > 100) {
|
||||||
|
this.scormHeight = this.scorm.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the attempt to use, the mode (normal/preview) and if it's offline or online.
|
||||||
|
*
|
||||||
|
* @param attemptsData Attempts count.
|
||||||
|
* @param accessInfo Access info.
|
||||||
|
*/
|
||||||
|
protected async determineAttemptAndMode(
|
||||||
|
attemptsData: AddonModScormAttemptCountResult,
|
||||||
|
accessInfo: AddonModScormGetScormAccessInformationWSResponse,
|
||||||
|
): Promise<void> {
|
||||||
|
const data = await AddonModScormHelper.determineAttemptToContinue(this.scorm, attemptsData);
|
||||||
|
|
||||||
|
let incomplete = false;
|
||||||
|
this.attempt = data.num;
|
||||||
|
|
||||||
|
// Check if current attempt is incomplete.
|
||||||
|
if (this.attempt > 0) {
|
||||||
|
incomplete = await AddonModScorm.isAttemptIncomplete(this.scorm.id, this.attempt, {
|
||||||
|
cmId: this.cmId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine mode and attempt to use.
|
||||||
|
const result = AddonModScorm.determineAttemptAndMode(
|
||||||
|
this.scorm,
|
||||||
|
this.mode,
|
||||||
|
this.attempt,
|
||||||
|
this.newAttempt,
|
||||||
|
incomplete,
|
||||||
|
accessInfo.cansavetrack,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.attempt > this.attempt) {
|
||||||
|
// We're creating a new attempt, verify that we can create a new online attempt. We ignore cache.
|
||||||
|
await AddonModScorm.getScormUserData(this.scorm.id, result.attempt, {
|
||||||
|
cmId: this.cmId,
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mode = result.mode;
|
||||||
|
this.newAttempt = result.newAttempt;
|
||||||
|
this.attempt = result.attempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data needed to play the SCORM.
|
||||||
|
*/
|
||||||
|
protected async fetchData(): Promise<void> {
|
||||||
|
await this.initialize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get attempts data.
|
||||||
|
const [attemptsData, accessInfo] = await Promise.all([
|
||||||
|
AddonModScorm.getAttemptCount(this.scorm.id, { cmId: this.cmId }),
|
||||||
|
AddonModScorm.getAccessInformation(this.scorm.id, {
|
||||||
|
cmId: this.cmId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.determineAttemptAndMode(attemptsData, accessInfo);
|
||||||
|
|
||||||
|
const sco = await this.getScoToLoad();
|
||||||
|
if (!sco) {
|
||||||
|
// We couldn't find a SCO to load: they're all inactive or without launch URL.
|
||||||
|
this.errorMessage = 'addon.mod_scorm.errornovalidsco';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load SCO.
|
||||||
|
this.src = await AddonModScorm.getScoSrcForOnlinePlayer(this.scorm, sco, {
|
||||||
|
mode: this.mode,
|
||||||
|
organization: this.organizationId,
|
||||||
|
newAttempt: this.newAttempt,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the TOC.
|
||||||
|
*
|
||||||
|
* @returns SCO to load.
|
||||||
|
*/
|
||||||
|
protected async getScoToLoad(): Promise<AddonModScormScoWithData | undefined> {
|
||||||
|
// We need to check incomplete again: attempt number or status might have changed.
|
||||||
|
const incomplete = await AddonModScorm.isAttemptIncomplete(this.scorm.id, this.attempt, {
|
||||||
|
cmId: this.cmId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get TOC.
|
||||||
|
const toc = await AddonModScormHelper.getToc(this.scorm.id, this.attempt, incomplete, {
|
||||||
|
organization: this.organizationId,
|
||||||
|
cmId: this.cmId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.newAttempt) {
|
||||||
|
// Creating a new attempt, use the first SCO defined by the SCORM.
|
||||||
|
this.initialScoId = this.scorm.launch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine current SCO if we received an ID.
|
||||||
|
let currentSco: AddonModScormScoWithData | undefined;
|
||||||
|
if (this.initialScoId && this.initialScoId > 0) {
|
||||||
|
// SCO set by parameter, get it from TOC.
|
||||||
|
currentSco = AddonModScormHelper.getScoFromToc(toc, this.initialScoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSco) {
|
||||||
|
return currentSco;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No SCO defined. Get the first valid one.
|
||||||
|
return await AddonModScormHelper.getFirstSco(this.scorm.id, this.attempt, {
|
||||||
|
toc,
|
||||||
|
organization: this.organizationId,
|
||||||
|
mode: this.mode,
|
||||||
|
cmId: this.cmId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
this.onlineObserver.unsubscribe();
|
||||||
|
|
||||||
|
// Empty src when leaving the state so unload event is triggered in the iframe.
|
||||||
|
this.src = '';
|
||||||
|
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'scorm' });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
||||||
import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu';
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
import { CoreSync } from '@services/sync';
|
import { CoreSync } from '@services/sync';
|
||||||
|
@ -89,10 +88,6 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
protected launchPrevObserver?: CoreEventObserver;
|
protected launchPrevObserver?: CoreEventObserver;
|
||||||
protected goOfflineObserver?: CoreEventObserver;
|
protected goOfflineObserver?: CoreEventObserver;
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected mainMenuPage: CoreMainMenuPage,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -311,8 +306,6 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch data needed to play the SCORM.
|
* Fetch data needed to play the SCORM.
|
||||||
*
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
protected async fetchData(): Promise<void> {
|
protected async fetchData(): Promise<void> {
|
||||||
if (!this.scorm) {
|
if (!this.scorm) {
|
||||||
|
|
|
@ -29,6 +29,10 @@ const routes: Routes = [
|
||||||
path: ':courseId/:cmId/player',
|
path: ':courseId/:cmId/player',
|
||||||
component: AddonModScormPlayerPage,
|
component: AddonModScormPlayerPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ':courseId/:cmId/online-player',
|
||||||
|
loadComponent: () => import('./pages/online-player/online-player'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { CoreFileSizeSum } from '@services/plugin-file-delegate';
|
||||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
import { CorePromiseUtils } from '@singletons/promise-utils';
|
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||||
import { CoreWSFile } from '@services/ws';
|
import { CoreWSFile } from '@services/ws';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { AddonModScorm, AddonModScormScorm } from '../scorm';
|
import { AddonModScorm, AddonModScormScorm } from '../scorm';
|
||||||
import { AddonModScormSync } from '../scorm-sync';
|
import { AddonModScormSync } from '../scorm-sync';
|
||||||
import { ADDON_MOD_SCORM_COMPONENT } from '../../constants';
|
import { ADDON_MOD_SCORM_COMPONENT } from '../../constants';
|
||||||
|
@ -177,10 +177,9 @@ export class AddonModScormPrefetchHandlerService extends CoreCourseActivityPrefe
|
||||||
|
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
const result = AddonModScorm.isScormUnsupported(scorm);
|
if (AddonModScorm.useOnlinePlayer(scorm)) {
|
||||||
|
// Shouldn't happen, if scorm uses online player it shouldn't be downloaded.
|
||||||
if (result) {
|
throw new CoreError('This SCORM cannot be downloaded.');
|
||||||
throw new CoreError(Translate.instant(result));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First verify that the file needs to be downloaded.
|
// First verify that the file needs to be downloaded.
|
||||||
|
@ -270,7 +269,7 @@ export class AddonModScormPrefetchHandlerService extends CoreCourseActivityPrefe
|
||||||
async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreFileSizeSum> {
|
async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreFileSizeSum> {
|
||||||
const scorm = await this.getScorm(module, courseId);
|
const scorm = await this.getScorm(module, courseId);
|
||||||
|
|
||||||
if (AddonModScorm.isScormUnsupported(scorm)) {
|
if (AddonModScorm.useOnlinePlayer(scorm)) {
|
||||||
return { size: -1, total: false };
|
return { size: -1, total: false };
|
||||||
} else if (!scorm.packagesize) {
|
} else if (!scorm.packagesize) {
|
||||||
// We don't have package size, try to calculate it.
|
// We don't have package size, try to calculate it.
|
||||||
|
@ -353,7 +352,7 @@ export class AddonModScormPrefetchHandlerService extends CoreCourseActivityPrefe
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AddonModScorm.isScormUnsupported(scorm)) {
|
if (AddonModScorm.useOnlinePlayer(scorm)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -962,6 +962,32 @@ export class AddonModScormProvider {
|
||||||
return CorePath.concatenatePaths(dirPath, launchUrl);
|
return CorePath.concatenatePaths(dirPath, launchUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a SCORM and a SCO, returns the full launch URL for the SCO to be used in an online player.
|
||||||
|
*
|
||||||
|
* @param scorm SCORM.
|
||||||
|
* @param sco SCO.
|
||||||
|
* @param options Other options.
|
||||||
|
* @returns The URL.
|
||||||
|
*/
|
||||||
|
async getScoSrcForOnlinePlayer(
|
||||||
|
scorm: AddonModScormScorm,
|
||||||
|
sco: AddonModScormWSSco,
|
||||||
|
options: AddonModScormGetScoSrcForOnlinePlayerOptions = {},
|
||||||
|
): Promise<string> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
// Use online player.
|
||||||
|
return CoreUrl.addParamsToUrl(CorePath.concatenatePaths(site.getURL(), '/mod/scorm/player.php'), {
|
||||||
|
a: scorm.id,
|
||||||
|
scoid: sco.id,
|
||||||
|
display: 'popup',
|
||||||
|
mode: options.mode,
|
||||||
|
currentorg: options.organization,
|
||||||
|
newattempt: options.newAttempt ? 'on' : 'off',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the folder where a SCORM is downloaded.
|
* Get the path to the folder where a SCORM is downloaded.
|
||||||
*
|
*
|
||||||
|
@ -985,7 +1011,7 @@ export class AddonModScormProvider {
|
||||||
getScormFileList(scorm: AddonModScormScorm): CoreWSFile[] {
|
getScormFileList(scorm: AddonModScormScorm): CoreWSFile[] {
|
||||||
const files: CoreWSFile[] = [];
|
const files: CoreWSFile[] = [];
|
||||||
|
|
||||||
if (!this.isScormUnsupported(scorm) && !scorm.warningMessage) {
|
if (!this.useOnlinePlayer(scorm) && !scorm.warningMessage) {
|
||||||
files.push({
|
files.push({
|
||||||
fileurl: this.getPackageUrl(scorm),
|
fileurl: this.getPackageUrl(scorm),
|
||||||
filepath: '/',
|
filepath: '/',
|
||||||
|
@ -1359,22 +1385,6 @@ export class AddonModScormProvider {
|
||||||
return !!(scorm.timeopen && scorm.timeopen > CoreTimeUtils.timestamp());
|
return !!(scorm.timeopen && scorm.timeopen > CoreTimeUtils.timestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a SCORM is unsupported in the app. If it's not, returns the error code to show.
|
|
||||||
*
|
|
||||||
* @param scorm SCORM to check.
|
|
||||||
* @returns String with error code if unsupported, undefined if supported.
|
|
||||||
*/
|
|
||||||
isScormUnsupported(scorm: AddonModScormScorm): string | undefined {
|
|
||||||
if (!this.isScormValidVersion(scorm)) {
|
|
||||||
return 'addon.mod_scorm.errorinvalidversion';
|
|
||||||
} else if (!this.isScormDownloadable(scorm)) {
|
|
||||||
return 'addon.mod_scorm.errornotdownloadable';
|
|
||||||
} else if (!this.isValidPackageUrl(this.getPackageUrl(scorm))) {
|
|
||||||
return 'addon.mod_scorm.errorpackagefile';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if it's a valid SCORM 1.2.
|
* Check if it's a valid SCORM 1.2.
|
||||||
*
|
*
|
||||||
|
@ -1698,6 +1708,17 @@ export class AddonModScormProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a SCORM should use an online player.
|
||||||
|
*
|
||||||
|
* @param scorm SCORM to check.
|
||||||
|
* @returns True if it should use an online player.
|
||||||
|
*/
|
||||||
|
useOnlinePlayer(scorm: AddonModScormScorm): boolean {
|
||||||
|
return !this.isScormValidVersion(scorm) || !this.isScormDownloadable(scorm) ||
|
||||||
|
!this.isValidPackageUrl(this.getPackageUrl(scorm));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddonModScorm = makeSingleton(AddonModScormProvider);
|
export const AddonModScorm = makeSingleton(AddonModScormProvider);
|
||||||
|
@ -2066,6 +2087,16 @@ export type AddonModScormScoIcon = {
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to pass to getScoSrcForOnlinePlayer.
|
||||||
|
*/
|
||||||
|
export type AddonModScormGetScoSrcForOnlinePlayerOptions = {
|
||||||
|
siteId?: string;
|
||||||
|
mode?: string; // Navigation mode.
|
||||||
|
organization?: string; // Organization ID.
|
||||||
|
newAttempt?: boolean; // Whether to start a new attempt.
|
||||||
|
};
|
||||||
|
|
||||||
declare module '@singletons/events' {
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue