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