forked from CIT/Vmeda.Online
		
	MOBILE-3653 scorm: Implement player page
This commit is contained in:
		
							parent
							
								
									a2cf8db2ea
								
							
						
					
					
						commit
						fd6e2a4a03
					
				
							
								
								
									
										1075
									
								
								src/addons/mod/scorm/classes/data-model-12.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1075
									
								
								src/addons/mod/scorm/classes/data-model-12.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -16,10 +16,12 @@ import { NgModule } from '@angular/core';
 | 
			
		||||
import { AddonModScormIndexComponent } from './index/index';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
 | 
			
		||||
import { AddonModScormTocComponent } from './toc/toc';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModScormIndexComponent,
 | 
			
		||||
        AddonModScormTocComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
@ -29,6 +31,7 @@ import { CoreCourseComponentsModule } from '@features/course/components/componen
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModScormIndexComponent,
 | 
			
		||||
        AddonModScormTocComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModScormComponentsModule {}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								src/addons/mod/scorm/components/toc/toc.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/addons/mod/scorm/components/toc/toc.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-title>{{ 'addon.mod_scorm.toc' | translate }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
 | 
			
		||||
                <ion-icon slot="icon-only" name="fas-times"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <nav>
 | 
			
		||||
        <ion-list class="addon-mod_scorm-toc">
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="attemptToContinue">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p>{{ 'addon.mod_scorm.dataattemptshown' | translate:{number: attemptToContinue} }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-center" *ngIf="isBrowse">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p>{{ 'addon.mod_scorm.browsemode' | translate }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-center" *ngIf="isReview">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p>{{ 'addon.mod_scorm.reviewmode' | translate }}</p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- List of SCOs. -->
 | 
			
		||||
            <ng-container *ngFor="let sco of toc">
 | 
			
		||||
                <ion-item *ngIf="sco.isvisible" class="ion-text-wrap" [detail]="sco.prereq && sco.launch"
 | 
			
		||||
                    [ngClass]="'core-padding-' + sco.level + ' addon-mod_scorm-type-' + sco.scormtype"
 | 
			
		||||
                    [class.core-selected-item]="selected == sco.id" (click)="loadSco(sco)"
 | 
			
		||||
                    [disabled]="!sco.prereq || !sco.launch ? true : null" tappable>
 | 
			
		||||
                    <ion-icon *ngIf="sco.icon" [name]="sco.icon.icon" [attr.aria-label]="sco.icon.description" slot="start">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <core-format-text [text]="sco.title" contextLevel="module" [contextInstanceId]="moduleId"
 | 
			
		||||
                            [courseId]="courseId">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                        <span *ngIf="accessInfo && accessInfo.canviewscores && sco.scoreraw">
 | 
			
		||||
                            ({{ 'addon.mod_scorm.score' | translate }}: {{sco.scoreraw}})
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
    </nav>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										68
									
								
								src/addons/mod/scorm/components/toc/toc.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/addons/mod/scorm/components/toc/toc.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
// (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, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { ModalController } from '@singletons';
 | 
			
		||||
import { AddonModScormGetScormAccessInformationWSResponse, AddonModScormProvider } from '../../services/scorm';
 | 
			
		||||
import { AddonModScormTOCScoWithIcon } from '../../services/scorm-helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Modal to display the TOC of a SCORM.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-scorm-toc',
 | 
			
		||||
    templateUrl: 'toc.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModScormTocComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @Input() toc: AddonModScormTOCScoWithIcon[] = [];
 | 
			
		||||
    @Input() attemptToContinue?: number;
 | 
			
		||||
    @Input() selected?: number;
 | 
			
		||||
    @Input() moduleId!: number;
 | 
			
		||||
    @Input() courseId!: number;
 | 
			
		||||
    @Input() accessInfo!: AddonModScormGetScormAccessInformationWSResponse;
 | 
			
		||||
    @Input() mode = '';
 | 
			
		||||
 | 
			
		||||
    isBrowse = false;
 | 
			
		||||
    isReview = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.isBrowse = this.mode === AddonModScormProvider.MODEBROWSE;
 | 
			
		||||
        this.isReview = this.mode === AddonModScormProvider.MODEREVIEW;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when a SCO is clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sco Clicked SCO.
 | 
			
		||||
     */
 | 
			
		||||
    loadSco(sco: AddonModScormTOCScoWithIcon): void {
 | 
			
		||||
        if (!sco.prereq || !sco.isvisible || !sco.launch) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ModalController.dismiss(sco);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close modal.
 | 
			
		||||
     */
 | 
			
		||||
    closeModal(): void {
 | 
			
		||||
        ModalController.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/addons/mod/scorm/pages/player/player.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/addons/mod/scorm/pages/player/player.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
<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]="cmId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button *ngIf="showToc && !loadingToc && toc.length" (click)="openToc()"
 | 
			
		||||
                [attr.aria-label]="'addon.mod_scorm.toc' | translate" aria-haspopup="true">
 | 
			
		||||
                <ion-icon name="fas-bookmark" slot="icon-only"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
            <ion-spinner *ngIf="showToc && loadingToc"></ion-spinner>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <core-navigation-bar [previous]="previousSco" [next]="nextSco" (action)="loadSco($event)"></core-navigation-bar>
 | 
			
		||||
 | 
			
		||||
        <core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"></core-iframe>
 | 
			
		||||
 | 
			
		||||
        <p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										574
									
								
								src/addons/mod/scorm/pages/player/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										574
									
								
								src/addons/mod/scorm/pages/player/player.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,574 @@
 | 
			
		||||
// (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 { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { ModalController } from '@singletons';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { AddonModScormDataModel12 } from '../../classes/data-model-12';
 | 
			
		||||
import { AddonModScormTocComponent } from '../../components/toc/toc';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModScorm,
 | 
			
		||||
    AddonModScormAttemptCountResult,
 | 
			
		||||
    AddonModScormGetScormAccessInformationWSResponse,
 | 
			
		||||
    AddonModScormProvider,
 | 
			
		||||
    AddonModScormScorm,
 | 
			
		||||
    AddonModScormScoWithData,
 | 
			
		||||
    AddonModScormUserDataMap,
 | 
			
		||||
} from '../../services/scorm';
 | 
			
		||||
import { AddonModScormHelper, AddonModScormTOCScoWithIcon } from '../../services/scorm-helper';
 | 
			
		||||
import { AddonModScormSync } from '../../services/scorm-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that allows playing a SCORM.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-scorm-player',
 | 
			
		||||
    templateUrl: 'player.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModScormPlayerPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    title?: string; // Title.
 | 
			
		||||
    scorm!: AddonModScormScorm; // The SCORM object.
 | 
			
		||||
    showToc = false; // Whether to show the table of contents (TOC).
 | 
			
		||||
    loadingToc = true; // Whether the TOC is being loaded.
 | 
			
		||||
    toc: AddonModScormTOCScoWithIcon[] = []; // List of SCOs.
 | 
			
		||||
    loaded = false; // Whether the data has been loaded.
 | 
			
		||||
    previousSco?: AddonModScormScoWithData; // Previous SCO.
 | 
			
		||||
    nextSco?: AddonModScormScoWithData; // Next SCO.
 | 
			
		||||
    src?: string; // Iframe src.
 | 
			
		||||
    errorMessage?: string; // Error message.
 | 
			
		||||
    accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
 | 
			
		||||
    scormWidth?: number; // Width applied to scorm iframe.
 | 
			
		||||
    scormHeight?: number; // Height applied to scorm iframe.
 | 
			
		||||
    incomplete = false; // Whether last attempt is incomplete.
 | 
			
		||||
    cmId!: number; // Course module ID.
 | 
			
		||||
    courseId!: number; // Course ID.
 | 
			
		||||
 | 
			
		||||
    protected siteId!: string;
 | 
			
		||||
    protected mode!: string; // 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?: number; // The attempt number.
 | 
			
		||||
    protected offline = false; // Whether it's offline mode.
 | 
			
		||||
    protected userData?: AddonModScormUserDataMap; // User data.
 | 
			
		||||
    protected initialScoId?: number; // Initial SCO ID to load.
 | 
			
		||||
    protected currentSco?: AddonModScormScoWithData; // Current SCO.
 | 
			
		||||
    protected dataModel?: AddonModScormDataModel12; // Data Model.
 | 
			
		||||
    protected attemptToContinue?: number; // Attempt to continue (for the popover).
 | 
			
		||||
 | 
			
		||||
    // Observers.
 | 
			
		||||
    protected tocObserver?: CoreEventObserver;
 | 
			
		||||
    protected launchNextObserver?: CoreEventObserver;
 | 
			
		||||
    protected launchPrevObserver?: CoreEventObserver;
 | 
			
		||||
    protected goOfflineObserver?: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected mainMenuPage: CoreMainMenuPage,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
 | 
			
		||||
        this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.mode = CoreNavigator.getRouteParam('mode') || AddonModScormProvider.MODENORMAL;
 | 
			
		||||
        this.moduleUrl = CoreNavigator.getRouteParam('moduleUrl') || '';
 | 
			
		||||
        this.newAttempt = !!CoreNavigator.getRouteBooleanParam('newAttempt');
 | 
			
		||||
        this.organizationId = CoreNavigator.getRouteParam('organizationId');
 | 
			
		||||
        this.initialScoId = CoreNavigator.getRouteNumberParam('scoId');
 | 
			
		||||
        this.siteId = CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Fetch the SCORM data.
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
 | 
			
		||||
            if (!this.currentSco) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Set start time if it's a new attempt.
 | 
			
		||||
            if (this.newAttempt) {
 | 
			
		||||
                try {
 | 
			
		||||
                    await this.setStartTime(this.currentSco.id);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Load SCO.
 | 
			
		||||
            this.loadSco(this.currentSco);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async initialize(): Promise<void> {
 | 
			
		||||
        // Get the SCORM instance.
 | 
			
		||||
        this.scorm = await AddonModScorm.getScorm(this.courseId, this.cmId, {
 | 
			
		||||
            moduleUrl: this.moduleUrl,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.PreferCache,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Block the SCORM so it cannot be synchronized.
 | 
			
		||||
        CoreSync.blockOperation(AddonModScormProvider.COMPONENT, this.scorm.id, 'player');
 | 
			
		||||
 | 
			
		||||
        // We use SCORM name at start, later we'll use the SCO title.
 | 
			
		||||
        this.title = this.scorm.name;
 | 
			
		||||
        this.showToc = AddonModScorm.displayTocInPlayer(this.scorm);
 | 
			
		||||
 | 
			
		||||
        if (this.scorm.popup) {
 | 
			
		||||
            this.mainMenuPage.changeVisibility(false);
 | 
			
		||||
 | 
			
		||||
            // If we receive a value > 100 we assume it's a fixed pixel size.
 | 
			
		||||
            if (this.scorm.width! > 100) {
 | 
			
		||||
                this.scormWidth = this.scorm.width;
 | 
			
		||||
 | 
			
		||||
                // Only get fixed size on height if width is also fixed.
 | 
			
		||||
                if (this.scorm.height! > 100) {
 | 
			
		||||
                    this.scormHeight = this.scorm.height;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Listen for events to update the TOC, navigate through SCOs and go offline.
 | 
			
		||||
        this.tocObserver = CoreEvents.on(AddonModScormProvider.UPDATE_TOC_EVENT, (data) => {
 | 
			
		||||
            if (data.scormId !== this.scorm.id) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.offline) {
 | 
			
		||||
                // Wait a bit to make sure data is stored.
 | 
			
		||||
                setTimeout(this.refreshToc.bind(this), 100);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.refreshToc();
 | 
			
		||||
            }
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
 | 
			
		||||
        this.launchNextObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => {
 | 
			
		||||
            if (data.scormId === this.scorm.id && this.nextSco) {
 | 
			
		||||
                this.loadSco(this.nextSco);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
 | 
			
		||||
        this.launchPrevObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => {
 | 
			
		||||
            if (data.scormId === this.scorm.id && this.previousSco) {
 | 
			
		||||
                this.loadSco(this.previousSco);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
 | 
			
		||||
        this.goOfflineObserver = CoreEvents.on(AddonModScormProvider.GO_OFFLINE_EVENT, (data) => {
 | 
			
		||||
            if (data.scormId !== this.scorm.id || this.offline) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            this.offline = true;
 | 
			
		||||
 | 
			
		||||
            // Wait a bit to prevent collisions between this store and SCORM API's store.
 | 
			
		||||
            setTimeout(async () => {
 | 
			
		||||
                try {
 | 
			
		||||
                    AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt!);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.refreshToc();
 | 
			
		||||
            }, 200);
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate the next and previous SCO.
 | 
			
		||||
     *
 | 
			
		||||
     * @param scoId Current SCO ID.
 | 
			
		||||
     */
 | 
			
		||||
    protected calculateNextAndPreviousSco(scoId: number): void {
 | 
			
		||||
        this.previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, scoId);
 | 
			
		||||
        this.nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, scoId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine the attempt to use, the mode (normal/preview) and if it's offline or online.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attemptsData Attempts count.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async determineAttemptAndMode(attemptsData: AddonModScormAttemptCountResult): Promise<void> {
 | 
			
		||||
        const data = await AddonModScormHelper.determineAttemptToContinue(this.scorm, attemptsData);
 | 
			
		||||
 | 
			
		||||
        let incomplete = false;
 | 
			
		||||
        this.attempt = data.num;
 | 
			
		||||
        this.offline = data.offline;
 | 
			
		||||
 | 
			
		||||
        if (this.attempt != attemptsData.lastAttempt.num) {
 | 
			
		||||
            this.attemptToContinue = this.attempt;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if current attempt is incomplete.
 | 
			
		||||
        if (this.attempt > 0) {
 | 
			
		||||
            incomplete = await AddonModScorm.isAttemptIncomplete(this.scorm.id, this.attempt, {
 | 
			
		||||
                offline: this.offline,
 | 
			
		||||
                cmId: this.cmId,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Determine mode and attempt to use.
 | 
			
		||||
        const result = AddonModScorm.determineAttemptAndMode(this.scorm, this.mode, this.attempt, this.newAttempt, incomplete);
 | 
			
		||||
 | 
			
		||||
        if (result.attempt > this.attempt) {
 | 
			
		||||
            // We're creating a new attempt.
 | 
			
		||||
            if (this.offline) {
 | 
			
		||||
                // Last attempt was offline, so we'll create a new offline attempt.
 | 
			
		||||
                await AddonModScormHelper.createOfflineAttempt(this.scorm, result.attempt, attemptsData.online.length);
 | 
			
		||||
            } else {
 | 
			
		||||
                try {
 | 
			
		||||
                    // Last attempt was online, 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.OnlyNetwork,
 | 
			
		||||
                    });
 | 
			
		||||
                } catch {
 | 
			
		||||
                    // Cannot communicate with the server, create an offline attempt.
 | 
			
		||||
                    this.offline = true;
 | 
			
		||||
 | 
			
		||||
                    await AddonModScormHelper.createOfflineAttempt(this.scorm, result.attempt, attemptsData.online.length);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.mode = result.mode;
 | 
			
		||||
        this.newAttempt = result.newAttempt;
 | 
			
		||||
        this.attempt = result.attempt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch data needed to play the SCORM.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        if (!this.scorm) {
 | 
			
		||||
            await this.initialize();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Wait for any ongoing sync to finish. We won't sync a SCORM while it's being played.
 | 
			
		||||
        await AddonModScormSync.waitForSync(this.scorm.id);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Get attempts data.
 | 
			
		||||
            const attemptsData = await AddonModScorm.getAttemptCount(this.scorm.id, { cmId: this.cmId });
 | 
			
		||||
 | 
			
		||||
            await this.determineAttemptAndMode(attemptsData);
 | 
			
		||||
 | 
			
		||||
            const [data, accessInfo] = await Promise.all([
 | 
			
		||||
                AddonModScorm.getScormUserData(this.scorm.id, this.attempt!, {
 | 
			
		||||
                    cmId: this.cmId,
 | 
			
		||||
                    offline: this.offline,
 | 
			
		||||
                }),
 | 
			
		||||
                AddonModScorm.getAccessInformation(this.scorm.id, {
 | 
			
		||||
                    cmId: this.cmId,
 | 
			
		||||
                }),
 | 
			
		||||
                this.fetchToc(),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            this.userData = data;
 | 
			
		||||
            this.accessInfo = accessInfo;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the TOC.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchToc(): Promise<void> {
 | 
			
		||||
        this.loadingToc = true;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // We need to check incomplete again: attempt number or status might have changed.
 | 
			
		||||
            this.incomplete = await AddonModScorm.isAttemptIncomplete(this.scorm.id, this.attempt!, {
 | 
			
		||||
                offline: this.offline,
 | 
			
		||||
                cmId: this.cmId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Get TOC.
 | 
			
		||||
            this.toc = await AddonModScormHelper.getToc(this.scorm.id, this.attempt!, this.incomplete, {
 | 
			
		||||
                organization: this.organizationId,
 | 
			
		||||
                offline: this.offline,
 | 
			
		||||
                cmId: this.cmId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (this.currentSco) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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.
 | 
			
		||||
            if (this.initialScoId && this.initialScoId > 0) {
 | 
			
		||||
                // SCO set by parameter, get it from TOC.
 | 
			
		||||
                this.currentSco = AddonModScormHelper.getScoFromToc(this.toc, this.initialScoId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.currentSco) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // No SCO defined. Get the first valid one.
 | 
			
		||||
            const sco = await AddonModScormHelper.getFirstSco(this.scorm.id, this.attempt!, {
 | 
			
		||||
                toc: this.toc,
 | 
			
		||||
                organization: this.organizationId,
 | 
			
		||||
                mode: this.mode,
 | 
			
		||||
                offline: this.offline,
 | 
			
		||||
                cmId: this.cmId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (sco) {
 | 
			
		||||
                this.currentSco = sco;
 | 
			
		||||
            } else {
 | 
			
		||||
                // We couldn't find a SCO to load: they're all inactive or without launch URL.
 | 
			
		||||
                this.errorMessage = 'addon.mod_scorm.errornovalidsco';
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loadingToc = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Page will leave.
 | 
			
		||||
     */
 | 
			
		||||
    ionViewWillLeave(): void {
 | 
			
		||||
        CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'scorm' });
 | 
			
		||||
 | 
			
		||||
        // Empty src when leaving the state so unload event is triggered in the iframe.
 | 
			
		||||
        this.src = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load a SCO.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sco The SCO to load.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async loadSco(sco: AddonModScormScoWithData): Promise<void> {
 | 
			
		||||
        if (!this.dataModel) {
 | 
			
		||||
            // Create the model.
 | 
			
		||||
            this.dataModel = new AddonModScormDataModel12(
 | 
			
		||||
                this.siteId,
 | 
			
		||||
                this.scorm,
 | 
			
		||||
                sco.id,
 | 
			
		||||
                this.attempt!,
 | 
			
		||||
                this.userData!,
 | 
			
		||||
                this.mode,
 | 
			
		||||
                this.offline,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Add the model to the window so the SCORM can access it.
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
            (<any> window).API = this.dataModel;
 | 
			
		||||
        } else {
 | 
			
		||||
            // Load the SCO in the existing model.
 | 
			
		||||
            this.dataModel.loadSco(sco.id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.currentSco = sco;
 | 
			
		||||
        this.title = sco.title || this.scorm.name; // Try to use SCO title.
 | 
			
		||||
 | 
			
		||||
        this.calculateNextAndPreviousSco(sco.id);
 | 
			
		||||
 | 
			
		||||
        // Load the SCO source.
 | 
			
		||||
        this.loadScoSrc(sco);
 | 
			
		||||
 | 
			
		||||
        if (sco.scormtype == 'asset') {
 | 
			
		||||
            // Mark the asset as completed.
 | 
			
		||||
            this.markCompleted(sco);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Trigger SCO launch event.
 | 
			
		||||
        CoreUtils.ignoreErrors(AddonModScorm.logLaunchSco(this.scorm.id, sco.id, this.scorm.name));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load SCO src.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sco SCO to load.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async loadScoSrc(sco: AddonModScormScoWithData): Promise<void> {
 | 
			
		||||
        const src = await AddonModScorm.getScoSrc(this.scorm, sco);
 | 
			
		||||
 | 
			
		||||
        if (src == this.src) {
 | 
			
		||||
            // Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed.
 | 
			
		||||
            this.src = '';
 | 
			
		||||
 | 
			
		||||
            await CoreUtils.nextTick();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.src = src;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given an SCO, mark it as completed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sco SCO to mark.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async markCompleted(sco: AddonModScormScoWithData): Promise<void> {
 | 
			
		||||
        const tracks = [{
 | 
			
		||||
            element: 'cmi.core.lesson_status',
 | 
			
		||||
            value: 'completed',
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            AddonModScorm.saveTracks(sco.id, this.attempt!, tracks, this.scorm, this.offline);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Error saving data. Go offline if needed.
 | 
			
		||||
            if (this.offline) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const data = await AddonModScorm.getScormUserData(this.scorm.id, this.attempt!, {
 | 
			
		||||
                cmId: this.cmId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (data[sco.id] && data[sco.id].userdata['cmi.core.lesson_status'] == 'completed') {
 | 
			
		||||
                // Already marked as completed.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                // Go offline.
 | 
			
		||||
                await AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt!);
 | 
			
		||||
 | 
			
		||||
                this.offline = true;
 | 
			
		||||
                this.dataModel?.setOffline(true);
 | 
			
		||||
 | 
			
		||||
                await AddonModScorm.saveTracks(sco.id, this.attempt!, tracks, this.scorm, true);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            // Refresh TOC, some prerequisites might have changed.
 | 
			
		||||
            this.refreshToc();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the TOC.
 | 
			
		||||
     */
 | 
			
		||||
    async openToc(): Promise<void> {
 | 
			
		||||
        const modal = await ModalController.create({
 | 
			
		||||
            component: AddonModScormTocComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                toc: this.toc,
 | 
			
		||||
                attemptToContinue: this.attemptToContinue,
 | 
			
		||||
                selected: this.currentSco && this.currentSco.id,
 | 
			
		||||
                moduleId: this.cmId,
 | 
			
		||||
                courseId: this.courseId,
 | 
			
		||||
                accessInfo: this.accessInfo,
 | 
			
		||||
                mode: this.mode,
 | 
			
		||||
            },
 | 
			
		||||
            cssClass: 'core-modal-lateral',
 | 
			
		||||
            showBackdrop: true,
 | 
			
		||||
            backdropDismiss: true,
 | 
			
		||||
            // @todo enterAnimation: 'core-modal-lateral-transition',
 | 
			
		||||
            // leaveAnimation: 'core-modal-lateral-transition'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await modal.present();
 | 
			
		||||
 | 
			
		||||
        const result = await modal.onDidDismiss();
 | 
			
		||||
 | 
			
		||||
        if (result.data) {
 | 
			
		||||
            this.loadSco(result.data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the TOC.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshToc(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreUtils.ignoreErrors(AddonModScorm.invalidateAllScormData(this.scorm.id));
 | 
			
		||||
 | 
			
		||||
            await this.fetchToc();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set SCORM start time.
 | 
			
		||||
     *
 | 
			
		||||
     * @param scoId SCO ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async setStartTime(scoId: number): Promise<void> {
 | 
			
		||||
        const tracks = [{
 | 
			
		||||
            element: 'x.start.time',
 | 
			
		||||
            value: String(CoreTimeUtils.timestamp()),
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
        await AddonModScorm.saveTracks(scoId, this.attempt!, tracks, this.scorm, this.offline);
 | 
			
		||||
 | 
			
		||||
        if (this.offline) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // New online attempt created, update cached data about online attempts.
 | 
			
		||||
        await CoreUtils.ignoreErrors(AddonModScorm.getAttemptCount(this.scorm.id, {
 | 
			
		||||
            cmId: this.cmId,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        // Stop listening for events.
 | 
			
		||||
        this.tocObserver?.off();
 | 
			
		||||
        this.launchNextObserver?.off();
 | 
			
		||||
        this.launchPrevObserver?.off();
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            this.goOfflineObserver?.off();
 | 
			
		||||
        }, 500);
 | 
			
		||||
 | 
			
		||||
        this.mainMenuPage.changeVisibility(true);
 | 
			
		||||
 | 
			
		||||
        // Unblock the SCORM so it can be synced.
 | 
			
		||||
        CoreSync.unblockOperation(AddonModScormProvider.COMPONENT, this.scorm.id, 'player');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -18,12 +18,17 @@ import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { AddonModScormComponentsModule } from './components/components.module';
 | 
			
		||||
import { AddonModScormIndexPage } from './pages/index/index';
 | 
			
		||||
import { AddonModScormPlayerPage } from './pages/player/player';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId',
 | 
			
		||||
        component: AddonModScormIndexPage,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/player',
 | 
			
		||||
        component: AddonModScormPlayerPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
@ -34,6 +39,7 @@ const routes: Routes = [
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModScormIndexPage,
 | 
			
		||||
        AddonModScormPlayerPage,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModScormLazyModule {}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user