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