MOBILE-2350 scorm: Implement index page and component
parent
b2bd25fdcf
commit
dffc8c3c6c
|
@ -16,7 +16,6 @@ import { Component, Optional, Injector } from '@angular/core';
|
||||||
import { Content, NavController } from 'ionic-angular';
|
import { Content, NavController } from 'ionic-angular';
|
||||||
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||||
import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
|
import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
|
||||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
|
||||||
import { AddonModQuizProvider } from '../../providers/quiz';
|
import { AddonModQuizProvider } from '../../providers/quiz';
|
||||||
import { AddonModQuizHelperProvider } from '../../providers/helper';
|
import { AddonModQuizHelperProvider } from '../../providers/helper';
|
||||||
import { AddonModQuizOfflineProvider } from '../../providers/quiz-offline';
|
import { AddonModQuizOfflineProvider } from '../../providers/quiz-offline';
|
||||||
|
@ -71,8 +70,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() content: Content,
|
constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() content: Content,
|
||||||
protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider,
|
protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider,
|
||||||
protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
|
protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
|
||||||
protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController,
|
protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController) {
|
||||||
protected prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
|
||||||
super(injector, content);
|
super(injector, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +115,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
// If the site doesn't support check updates, always prefetch it because we cannot tell if there's something new.
|
// If the site doesn't support check updates, always prefetch it because we cannot tell if there's something new.
|
||||||
const isDownloaded = this.currentStatus == CoreConstants.DOWNLOADED;
|
const isDownloaded = this.currentStatus == CoreConstants.DOWNLOADED;
|
||||||
|
|
||||||
if (!isDownloaded || !this.prefetchDelegate.canCheckUpdates()) {
|
if (!isDownloaded || !this.modulePrefetchDelegate.canCheckUpdates()) {
|
||||||
// Prefetch the quiz.
|
// Prefetch the quiz.
|
||||||
this.showStatusSpinner = true;
|
this.showStatusSpinner = true;
|
||||||
|
|
||||||
|
@ -125,7 +123,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
// Success downloading, open quiz.
|
// Success downloading, open quiz.
|
||||||
this.openQuiz();
|
this.openQuiz();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (this.hasOffline || (isDownloaded && !this.prefetchDelegate.canCheckUpdates())) {
|
if (this.hasOffline || (isDownloaded && !this.modulePrefetchDelegate.canCheckUpdates())) {
|
||||||
// Error downloading but there is something offline, allow continuing it.
|
// Error downloading but there is something offline, allow continuing it.
|
||||||
// If the site doesn't support check updates, continue too because we cannot tell if there's something new.
|
// If the site doesn't support check updates, continue too because we cannot tell if there's something new.
|
||||||
this.openQuiz();
|
this.openQuiz();
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||||
|
import { AddonModScormIndexComponent } from './index/index';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModScormIndexComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreCourseComponentsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModScormIndexComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModScormIndexComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModScormComponentsModule {}
|
|
@ -0,0 +1,170 @@
|
||||||
|
<!-- Buttons to add to the header. -->
|
||||||
|
<core-navbar-buttons end>
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></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" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></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"></core-course-module-description>
|
||||||
|
|
||||||
|
<!-- Warning message. -->
|
||||||
|
<div *ngIf="scorm && scorm.warningMessage" class="core-info-card" icon-start>
|
||||||
|
<ion-icon name="information"></ion-icon>
|
||||||
|
{{ scorm.warningMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="scorm && loaded && !scorm.warningMessage">
|
||||||
|
<!-- Attempts status. -->
|
||||||
|
<ion-card *ngIf="scorm.displayattemptstatus || Object.keys(scorm.offlineAttempts).length">
|
||||||
|
<ion-card-header text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_scorm.attempts' | translate }}</h2>
|
||||||
|
</ion-card-header>
|
||||||
|
<ion-list>
|
||||||
|
<ng-container *ngIf="scorm.displayattemptstatus">
|
||||||
|
<ion-item text-wrap *ngIf="scorm.maxattempt >= 0">
|
||||||
|
<p class="item-heading">{{ 'addon.mod_scorm.noattemptsallowed' | translate }}</p>
|
||||||
|
<p *ngIf="scorm.maxattempt == 0">{{ 'core.unlimited' | translate }}</p>
|
||||||
|
<p *ngIf="scorm.maxattempt > 0">{{ scorm.maxattempt }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="scorm.numAttempts >= 0">
|
||||||
|
<p class="item-heading">{{ 'addon.mod_scorm.noattemptsmade' | translate }}</p>
|
||||||
|
<p>{{ scorm.numAttempts }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngFor="let attempt of scorm.onlineAttempts">
|
||||||
|
<p class="item-heading">{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}</p>
|
||||||
|
<p *ngIf="attempt.grade != -1">{{ attempt.grade }}</p>
|
||||||
|
<p *ngIf="attempt.grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
<ion-item text-wrap *ngFor="let attempt of scorm.offlineAttempts">
|
||||||
|
<p class="item-heading">{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}</p>
|
||||||
|
<p *ngIf="attempt.grade != -1">{{ attempt.grade }}</p>
|
||||||
|
<p *ngIf="attempt.grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</p>
|
||||||
|
<p *ngIf="scorm.maxattempt == 0 || attempt.number <= scorm.maxattempt">{{ 'addon.mod_scorm.offlineattemptnote' | translate }}</p>
|
||||||
|
<p *ngIf="scorm.maxattempt != 0 && attempt.number > scorm.maxattempt">{{ 'addon.mod_scorm.offlineattemptovermax' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="scorm.displayattemptstatus && scorm.gradeMethodReadable">
|
||||||
|
<p class="item-heading">{{ 'addon.mod_scorm.grademethod' | translate }}</p>
|
||||||
|
<p>{{ scorm.gradeMethodReadable }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="scorm.displayattemptstatus && scorm.grade">
|
||||||
|
<p class="item-heading">{{ 'addon.mod_scorm.gradereported' | translate }}</p>
|
||||||
|
<p *ngIf="scorm.grade != -1">{{ scorm.grade }}</p>
|
||||||
|
<p *ngIf="scorm.grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="syncTime">
|
||||||
|
<p class="item-heading">{{ 'core.lastsync' | translate }}</p>
|
||||||
|
<p>{{ syncTime }}</p>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Synchronization warning. -->
|
||||||
|
<div class="core-warning-card" icon-start *ngIf="!errorMessage && hasOffline">
|
||||||
|
<ion-icon name="warning"></ion-icon>
|
||||||
|
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOC. -->
|
||||||
|
<ion-card *ngIf="scorm && organizations && ((scorm.displaycoursestructure && organizations.length) || organizations.length > 1)" class="addon-mod_scorm-toc">
|
||||||
|
<ion-card-header text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_scorm.contents' | translate }}</h2>
|
||||||
|
</ion-card-header>
|
||||||
|
<ion-list>
|
||||||
|
<ion-item text-wrap *ngIf="organizations.length > 1">
|
||||||
|
<ion-label>{{ 'addon.mod_scorm.organizations' | translate }}</ion-label>
|
||||||
|
<ion-select [(ngModel)]="currentOrganization.identifier" (ionChange)="loadOrganization()" interface="popover">
|
||||||
|
<ion-option *ngFor="let org of organizations" [value]="org.identifier">{{ org.title }}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-center *ngIf="scorm.displaycoursestructure && loadingToc">
|
||||||
|
<ion-spinner></ion-spinner>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="scorm.displaycoursestructure && !loadingToc">
|
||||||
|
<!-- If data shown doesn't belong to last attempt, show a warning. -->
|
||||||
|
<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}}">
|
||||||
|
<p *ngIf="sco.isvisible">
|
||||||
|
<img [src]="sco.image.url" [alt]="sco.image.description" />
|
||||||
|
<a *ngIf="sco.prereq && sco.launch" (click)="open($event, sco.id)">{{ sco.title }}</a>
|
||||||
|
<span *ngIf="!sco.prereq || !sco.launch">{{ sco.title }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Open in browser button. -->
|
||||||
|
<ion-card *ngIf="errorMessage">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<p class="text-danger">{{ errorMessage | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<a ion-button block icon-end [href]="externalUrl" core-link>
|
||||||
|
{{ 'core.openinbrowser' | translate }}
|
||||||
|
<ion-icon name="open"></ion-icon>
|
||||||
|
</a>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Warning that user doesn't have any more attempts. -->
|
||||||
|
<ion-card *ngIf="!errorMessage && scorm && scorm.attemptsLeft <= 0">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<p class="text-danger">{{ 'addon.mod_scorm.exceededmaxattempts' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Open SCORM in app form -->
|
||||||
|
<ion-card *ngIf="!errorMessage && scorm && (!scorm.lastattemptlock || scorm.attemptsLeft > 0)">
|
||||||
|
<ion-list>
|
||||||
|
<!-- Open mode (Preview or Normal) -->
|
||||||
|
<div *ngIf="!scorm.hidebrowse" radio-group [(ngModel)]="scormOptions.mode" name="mode">
|
||||||
|
<ion-item>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_scorm.mode' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>{{ 'addon.mod_scorm.browse' | translate }}</ion-label>
|
||||||
|
<ion-radio [value]="modeBrowser"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>{{ 'addon.mod_scorm.normal' | translate }}</ion-label>
|
||||||
|
<ion-radio [value]="modeNormal"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create new attempt -->
|
||||||
|
<ion-item text-wrap *ngIf="!scorm.forcenewattempt && scorm.numAttempts > 0 && !scorm.incomplete && scorm.attemptsLeft > 0">
|
||||||
|
<ion-label>{{ 'addon.mod_scorm.newattempt' | translate }}</ion-label>
|
||||||
|
<ion-checkbox item-end name="newAttempt" [(ngModel)]="scormOptions.newAttempt">
|
||||||
|
</ion-checkbox>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Button to open the SCORM. -->
|
||||||
|
<ng-container *ngIf="!downloading">
|
||||||
|
<ion-item text-wrap *ngIf="statusMessage">
|
||||||
|
<p >{{ statusMessage | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<a ion-button block (click)="open($event)">{{ 'addon.mod_scorm.enter' | translate }}</a>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Download progress. -->
|
||||||
|
<ion-item text-center *ngIf="downloading">
|
||||||
|
<ion-spinner></ion-spinner>
|
||||||
|
<p *ngIf="progressMessage">{{ progressMessage | translate }}</p>
|
||||||
|
<p *ngIf="percentage <= 100">{{ 'core.percentagenumber' | translate:{$a: percentage} }}</p>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-card>
|
||||||
|
</div>
|
||||||
|
</core-loading>
|
|
@ -0,0 +1,9 @@
|
||||||
|
addon-mod-scorm-index {
|
||||||
|
|
||||||
|
.addon-mod_scorm-toc {
|
||||||
|
img {
|
||||||
|
width: auto;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,531 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Optional, Injector } from '@angular/core';
|
||||||
|
import { Content, NavController } from 'ionic-angular';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||||
|
import { AddonModScormProvider, AddonModScormAttemptCountResult } from '../../providers/scorm';
|
||||||
|
import { AddonModScormHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModScormOfflineProvider } from '../../providers/scorm-offline';
|
||||||
|
import { AddonModScormSyncProvider } from '../../providers/scorm-sync';
|
||||||
|
import { AddonModScormPrefetchHandler } from '../../providers/prefetch-handler';
|
||||||
|
import { CoreConstants } from '@core/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a SCORM entry page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-scorm-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityComponent {
|
||||||
|
component = AddonModScormProvider.COMPONENT;
|
||||||
|
moduleName = 'scorm';
|
||||||
|
|
||||||
|
scorm: any; // The SCORM object.
|
||||||
|
currentOrganization: any = {}; // Selected organization.
|
||||||
|
scormOptions: any = { // Options to open the SCORM.
|
||||||
|
mode: AddonModScormProvider.MODENORMAL,
|
||||||
|
newAttempt: false
|
||||||
|
};
|
||||||
|
modeNormal = AddonModScormProvider.MODENORMAL; // Normal open mode.
|
||||||
|
modeBrowser = AddonModScormProvider.MODEBROWSE; // Browser open mode.
|
||||||
|
errorMessage: string; // Error message.
|
||||||
|
syncTime: string; // Last sync time.
|
||||||
|
hasOffline: boolean; // Whether the SCORM has offline data.
|
||||||
|
attemptToContinue: number; // The attempt to continue or review.
|
||||||
|
statusMessage: string; // Message about the status.
|
||||||
|
downloading: boolean; // Whether the SCORM is being downloaded.
|
||||||
|
percentage: string; // Download/unzip percentage.
|
||||||
|
progressMessage: string; // Message about download/unzip.
|
||||||
|
organizations: any[]; // List of organizations.
|
||||||
|
loadingToc: boolean; // Whether the TOC is being loaded.
|
||||||
|
toc: any[]; // Table of contents (structure).
|
||||||
|
|
||||||
|
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: boolean; // Whether the last attempt is offline.
|
||||||
|
protected hasPlayed = false; // Whether the user has opened the player page.
|
||||||
|
|
||||||
|
constructor(injector: Injector, protected scormProvider: AddonModScormProvider, @Optional() protected content: Content,
|
||||||
|
protected scormHelper: AddonModScormHelperProvider, protected scormOffline: AddonModScormOfflineProvider,
|
||||||
|
protected scormSync: AddonModScormSyncProvider, protected prefetchHandler: AddonModScormPrefetchHandler,
|
||||||
|
protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate,
|
||||||
|
protected utils: CoreUtilsProvider) {
|
||||||
|
super(injector, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.loadContent(false, true).then(() => {
|
||||||
|
if (!this.scorm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scormProvider.logView(this.scorm.id).then(() => {
|
||||||
|
this.checkCompletion();
|
||||||
|
}).catch((error) => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the completion.
|
||||||
|
*/
|
||||||
|
protected checkCompletion(): void {
|
||||||
|
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a SCORM package or restores an ongoing download.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected downloadScormPackage(): Promise<any> {
|
||||||
|
this.downloading = true;
|
||||||
|
|
||||||
|
return this.prefetchHandler.download(this.module, this.courseId, undefined, (data) => {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.downloading) {
|
||||||
|
// Downloading package.
|
||||||
|
if (this.scorm.packagesize && data.progress) {
|
||||||
|
this.percentage = (Number(data.progress.loaded / this.scorm.packagesize) * 100).toFixed(1);
|
||||||
|
}
|
||||||
|
} else if (data.message) {
|
||||||
|
// Show a message.
|
||||||
|
this.progressMessage = data.message;
|
||||||
|
this.percentage = undefined;
|
||||||
|
} else if (data.progress && data.progress.loaded && data.progress.total) {
|
||||||
|
// Unzipping package.
|
||||||
|
this.percentage = (Number(data.progress.loaded / data.progress.total) * 100).toFixed(1);
|
||||||
|
} else {
|
||||||
|
this.percentage = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).finally(() => {
|
||||||
|
this.progressMessage = undefined;
|
||||||
|
this.percentage = undefined;
|
||||||
|
this.downloading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the SCORM data.
|
||||||
|
*
|
||||||
|
* @param {boolean} [refresh=false] If it's refreshing content.
|
||||||
|
* @param {boolean} [sync=false] If the refresh is needs syncing.
|
||||||
|
* @param {boolean} [showErrors=false] If show errors to the user of hide them.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||||
|
|
||||||
|
// Get the SCORM instance.
|
||||||
|
return this.scormProvider.getScorm(this.courseId, this.module.id, this.module.url).then((scormData) => {
|
||||||
|
this.scorm = scormData;
|
||||||
|
|
||||||
|
this.dataRetrieved.emit(this.scorm);
|
||||||
|
this.description = this.scorm.intro || this.description;
|
||||||
|
|
||||||
|
const result = this.scormProvider.isScormUnsupported(this.scorm);
|
||||||
|
if (result) {
|
||||||
|
this.errorMessage = result;
|
||||||
|
} else {
|
||||||
|
this.errorMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scorm.warningMessage) {
|
||||||
|
return; // SCORM is closed or not open yet, we can't get more data.
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise;
|
||||||
|
if (sync) {
|
||||||
|
// Try to synchronize the assign.
|
||||||
|
promise = this.syncActivity(showErrors).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.catch(() => {
|
||||||
|
// Ignore errors, keep getting data even if sync fails.
|
||||||
|
}).then(() => {
|
||||||
|
|
||||||
|
// No need to return this promise, it should be faster than the rest.
|
||||||
|
this.scormSync.getReadableSyncTime(this.scorm.id).then((syncTime) => {
|
||||||
|
this.syncTime = syncTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the number of attempts.
|
||||||
|
return this.scormProvider.getAttemptCount(this.scorm.id);
|
||||||
|
}).then((attemptsData) => {
|
||||||
|
this.attempts = attemptsData;
|
||||||
|
this.hasOffline = !!this.attempts.offline.length;
|
||||||
|
|
||||||
|
// Determine the attempt that will be continued or reviewed.
|
||||||
|
return this.scormHelper.determineAttemptToContinue(this.scorm, this.attempts);
|
||||||
|
}).then((attempt) => {
|
||||||
|
this.lastAttempt = attempt.number;
|
||||||
|
this.lastIsOffline = attempt.offline;
|
||||||
|
|
||||||
|
if (this.lastAttempt != this.attempts.lastAttempt.number) {
|
||||||
|
this.attemptToContinue = this.lastAttempt;
|
||||||
|
} else {
|
||||||
|
this.attemptToContinue = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the last attempt is incomplete.
|
||||||
|
return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.lastAttempt, this.lastIsOffline);
|
||||||
|
}).then((incomplete) => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
this.scorm.incomplete = incomplete;
|
||||||
|
this.scorm.numAttempts = this.attempts.total;
|
||||||
|
this.scorm.gradeMethodReadable = this.scormProvider.getScormGradeMethod(this.scorm);
|
||||||
|
this.scorm.attemptsLeft = this.scormProvider.countAttemptsLeft(this.scorm, this.attempts.lastAttempt.number);
|
||||||
|
if (this.scorm.forceattempt && this.scorm.incomplete) {
|
||||||
|
this.scormOptions.newAttempt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(this.getReportedGrades());
|
||||||
|
|
||||||
|
promises.push(this.fetchStructure());
|
||||||
|
|
||||||
|
if (!this.scorm.packagesize && this.errorMessage === '') {
|
||||||
|
// SCORM is supported but we don't have package size. Try to calculate it.
|
||||||
|
promises.push(this.scormProvider.calculateScormSize(this.scorm).then((size) => {
|
||||||
|
this.scorm.packagesize = size;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle status.
|
||||||
|
this.setStatusListener();
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
// All data obtained, now fill the context menu.
|
||||||
|
this.fillContextMenu(refresh);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the structure of the SCORM (TOC).
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchStructure(): Promise<any> {
|
||||||
|
return this.scormProvider.getOrganizations(this.scorm.id).then((organizations) => {
|
||||||
|
this.organizations = organizations;
|
||||||
|
|
||||||
|
if (!this.currentOrganization.identifier) {
|
||||||
|
// Load first organization (if any).
|
||||||
|
if (organizations.length) {
|
||||||
|
this.currentOrganization.identifier = organizations[0].identifier;
|
||||||
|
} else {
|
||||||
|
this.currentOrganization.identifier = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadOrganizationToc(this.currentOrganization.identifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the grade of an attempt and add it to the scorm attempts list.
|
||||||
|
*
|
||||||
|
* @param {number} attempt The attempt number.
|
||||||
|
* @param {boolean} offline Whether it's an offline attempt.
|
||||||
|
* @param {any} attempts Object where to add the attempt.
|
||||||
|
* @return {Promise<void>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected getAttemptGrade(attempt: number, offline: boolean, attempts: any): Promise<void> {
|
||||||
|
return this.scormProvider.getAttemptGrade(this.scorm, attempt, offline).then((grade) => {
|
||||||
|
attempts[attempt] = {
|
||||||
|
number: attempt,
|
||||||
|
grade: grade
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the grades of each attempt and the grade of the SCORM.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected getReportedGrades(): Promise<any> {
|
||||||
|
const promises = [],
|
||||||
|
onlineAttempts = {},
|
||||||
|
offlineAttempts = {};
|
||||||
|
|
||||||
|
// Calculate the grade for each attempt.
|
||||||
|
this.attempts.online.forEach((attempt) => {
|
||||||
|
// Check that attempt isn't in offline to prevent showing the same attempt twice. Offline should be more recent.
|
||||||
|
if (this.attempts.offline.indexOf(attempt) == -1) {
|
||||||
|
promises.push(this.getAttemptGrade(attempt, false, onlineAttempts));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.attempts.offline.forEach((attempt) => {
|
||||||
|
promises.push(this.getAttemptGrade(attempt, true, offlineAttempts));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
|
||||||
|
// Calculate the grade of the whole SCORM. We only use online attempts to calculate this data.
|
||||||
|
this.scorm.grade = this.scormProvider.calculateScormGrade(this.scorm, onlineAttempts);
|
||||||
|
|
||||||
|
// Add the attempts to the SCORM in array format in ASC order, and format the grades.
|
||||||
|
this.scorm.onlineAttempts = this.utils.objectToArray(onlineAttempts);
|
||||||
|
this.scorm.offlineAttempts = this.utils.objectToArray(offlineAttempts);
|
||||||
|
this.scorm.onlineAttempts.sort((a, b) => {
|
||||||
|
return a.number - b.number;
|
||||||
|
});
|
||||||
|
this.scorm.offlineAttempts.sort((a, b) => {
|
||||||
|
return a.number - b.number;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now format the grades.
|
||||||
|
this.scorm.onlineAttempts.forEach((attempt) => {
|
||||||
|
attempt.grade = this.scormProvider.formatGrade(this.scorm, attempt.grade);
|
||||||
|
});
|
||||||
|
this.scorm.offlineAttempts.forEach((attempt) => {
|
||||||
|
attempt.grade = this.scormProvider.formatGrade(this.scorm, attempt.grade);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scorm.grade = this.scormProvider.formatGrade(this.scorm, this.scorm.grade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sync has succeed from result sync data.
|
||||||
|
*
|
||||||
|
* @param {any} result Data returned on the sync function.
|
||||||
|
* @return {boolean} If suceed or not.
|
||||||
|
*/
|
||||||
|
protected hasSyncSucceed(result: any): boolean {
|
||||||
|
if (result.updated) {
|
||||||
|
// Check completion status.
|
||||||
|
this.checkCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page that contains the component.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
super.ionViewDidEnter();
|
||||||
|
|
||||||
|
if (this.hasPlayed) {
|
||||||
|
this.hasPlayed = false;
|
||||||
|
this.scormOptions.newAttempt = false; // Uncheck new attempt.
|
||||||
|
|
||||||
|
// Add a delay to make sure the player has started the last writing calls so we can detect conflicts.
|
||||||
|
setTimeout(() => {
|
||||||
|
// Refresh data.
|
||||||
|
this.showLoadingAndRefresh(true, false);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page that contains the component.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
super.ionViewDidLeave();
|
||||||
|
|
||||||
|
if (this.navCtrl.getActive().component.name == 'AddonModScormPlayerPage') {
|
||||||
|
this.hasPlayed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected invalidateContent(): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.scormProvider.invalidateScormData(this.courseId));
|
||||||
|
|
||||||
|
if (this.scorm) {
|
||||||
|
promises.push(this.scormProvider.invalidateAllScormData(this.scorm.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares sync event data with current data to check if refresh content is needed.
|
||||||
|
*
|
||||||
|
* @param {any} syncEventData Data receiven on sync observer.
|
||||||
|
* @return {boolean} True if refresh is needed, false otherwise.
|
||||||
|
*/
|
||||||
|
protected isRefreshSyncNeeded(syncEventData: any): 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.
|
||||||
|
*/
|
||||||
|
loadOrganization(): void {
|
||||||
|
this.loadOrganizationToc(this.currentOrganization.identifier).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the TOC of a certain organization.
|
||||||
|
*
|
||||||
|
* @param {string} organizationId The organization id.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected loadOrganizationToc(organizationId: string): Promise<any> {
|
||||||
|
if (!this.scorm.displaycoursestructure) {
|
||||||
|
// TOC is not displayed, no need to load it.
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingToc = true;
|
||||||
|
|
||||||
|
return this.scormProvider.getOrganizationToc(this.scorm.id, this.lastAttempt, organizationId, this.lastIsOffline)
|
||||||
|
.then((toc) => {
|
||||||
|
|
||||||
|
this.toc = this.scormProvider.formatTocToArray(toc);
|
||||||
|
|
||||||
|
// Get images for each SCO.
|
||||||
|
this.toc.forEach((sco) => {
|
||||||
|
sco.image = this.scormProvider.getScoStatusIcon(sco, this.scorm.incomplete);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// The scoId param indicates the SCO that needs to be loaded when the SCORM is opened. If not defined, load first SCO.
|
||||||
|
open(e: Event, scoId: number): void {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this.downloading) {
|
||||||
|
// Scope is being downloaded, abort.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOutdated = this.currentStatus == CoreConstants.OUTDATED;
|
||||||
|
|
||||||
|
if (isOutdated || this.currentStatus == CoreConstants.NOT_DOWNLOADED) {
|
||||||
|
// SCORM needs to be downloaded.
|
||||||
|
this.scormHelper.confirmDownload(this.scorm, isOutdated).then(() => {
|
||||||
|
// Invalidate WS data if SCORM is outdated.
|
||||||
|
const promise = isOutdated ? this.scormProvider.invalidateAllScormData(this.scorm.id) : Promise.resolve();
|
||||||
|
|
||||||
|
promise.finally(() => {
|
||||||
|
this.downloadScormPackage().then(() => {
|
||||||
|
// Success downloading, open SCORM if user hasn't left the view.
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
this.openScorm(scoId);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
this.domUtils.showErrorModalDefault(error, this.translate.instant(
|
||||||
|
'addon.mod_scorm.errordownloadscorm', {name: this.scorm.name}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.openScorm(scoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a SCORM package.
|
||||||
|
*
|
||||||
|
* @param {number} scoId SCO ID.
|
||||||
|
*/
|
||||||
|
protected openScorm(scoId: number): void {
|
||||||
|
this.navCtrl.push('AddonModScormPlayerPage', {
|
||||||
|
scorm: this.scorm,
|
||||||
|
mode: this.scormOptions.mode,
|
||||||
|
newAttempt: !!this.scormOptions.newAttempt,
|
||||||
|
organizationId: this.currentOrganization.identifier,
|
||||||
|
scoId: scoId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays some data based on the current status.
|
||||||
|
*
|
||||||
|
* @param {string} status The current status.
|
||||||
|
* @param {string} [previousStatus] The previous status. If not defined, there is no previous status.
|
||||||
|
*/
|
||||||
|
protected showStatus(status: string, previousStatus?: string): void {
|
||||||
|
|
||||||
|
if (status == CoreConstants.OUTDATED && this.scorm) {
|
||||||
|
// Only show the outdated message if the file should be downloaded.
|
||||||
|
this.scormProvider.shouldDownloadMainFile(this.scorm, true).then((download) => {
|
||||||
|
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<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected sync(): Promise<any> {
|
||||||
|
return this.scormSync.syncScorm(this.scorm);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||||
|
|
||||||
|
<ion-buttons end>
|
||||||
|
<!-- The buttons defined by the component will be added in here. -->
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="scormComponent.loaded" (ionRefresh)="scormComponent.doRefresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<addon-mod-scorm-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-scorm-index>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModScormComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModScormIndexPage } from './index';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModScormIndexPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
AddonModScormComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModScormIndexPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModScormIndexPageModule {}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, ViewChild } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { AddonModScormIndexComponent } from '../../components/index/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the SCORM entry page.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-scorm-index' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-scorm-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModScormIndexPage {
|
||||||
|
@ViewChild(AddonModScormIndexComponent) scormComponent: AddonModScormIndexComponent;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
module: any;
|
||||||
|
courseId: number;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams) {
|
||||||
|
this.module = navParams.get('module') || {};
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.title = this.module.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update some data based on the SCORM instance.
|
||||||
|
*
|
||||||
|
* @param {any} scorm SCORM instance.
|
||||||
|
*/
|
||||||
|
updateData(scorm: any): void {
|
||||||
|
this.title = scorm.name || this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.scormComponent.ionViewDidEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
this.scormComponent.ionViewDidLeave();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper service that provides some features for SCORM.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModScormHelperProvider {
|
||||||
|
|
||||||
|
protected div = document.createElement('div'); // A div element to search in HTML code.
|
||||||
|
|
||||||
|
constructor(private domUtils: CoreDomUtilsProvider, private scormProvider: AddonModScormProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a confirm dialog if needed. If SCORM doesn't have size, try to calculate it.
|
||||||
|
*
|
||||||
|
* @param {any} scorm SCORM to download.
|
||||||
|
* @param {boolean} [isOutdated] True if package outdated, false if not outdated, undefined to calculate it.
|
||||||
|
* @return {Promise<any>} Promise resolved if the user confirms or no confirmation needed.
|
||||||
|
*/
|
||||||
|
confirmDownload(scorm: any, isOutdated?: boolean): Promise<any> {
|
||||||
|
// Check if file should be downloaded.
|
||||||
|
return this.scormProvider.shouldDownloadMainFile(scorm, isOutdated).then((download) => {
|
||||||
|
if (download) {
|
||||||
|
let subPromise;
|
||||||
|
|
||||||
|
if (!scorm.packagesize) {
|
||||||
|
// We don't have package size, try to calculate it.
|
||||||
|
subPromise = this.scormProvider.calculateScormSize(scorm).then((size) => {
|
||||||
|
// Store it so we don't have to calculate it again when using the same object.
|
||||||
|
scorm.packagesize = size;
|
||||||
|
|
||||||
|
return size;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
subPromise = Promise.resolve(scorm.packagesize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return subPromise.then((size) => {
|
||||||
|
return this.domUtils.confirmDownloadSize({size: size, total: true});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the attempt to continue/review. It will be:
|
||||||
|
* - The last incomplete online attempt if it hasn't been continued in offline and all offline attempts are complete.
|
||||||
|
* - The attempt with highest number without surpassing max attempts otherwise.
|
||||||
|
*
|
||||||
|
* @param {any} scorm SCORM object.
|
||||||
|
* @param {AddonModScormAttemptCountResult} attempts Attempts count.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<{number: number, offline: boolean}>} Promise resolved with the attempt data.
|
||||||
|
*/
|
||||||
|
determineAttemptToContinue(scorm: any, attempts: AddonModScormAttemptCountResult, siteId?: string)
|
||||||
|
: Promise<{number: number, offline: boolean}> {
|
||||||
|
|
||||||
|
let lastOnline;
|
||||||
|
|
||||||
|
// Get last online attempt.
|
||||||
|
if (attempts.online.length) {
|
||||||
|
lastOnline = Math.max.apply(Math, attempts.online);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastOnline) {
|
||||||
|
// Check if last online incomplete.
|
||||||
|
const hasOffline = attempts.offline.indexOf(lastOnline) > -1;
|
||||||
|
|
||||||
|
return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, hasOffline, false, siteId).then((incomplete) => {
|
||||||
|
if (incomplete) {
|
||||||
|
return {
|
||||||
|
number: lastOnline,
|
||||||
|
offline: hasOffline
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return this.getLastBeforeMax(scorm, attempts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(this.getLastBeforeMax(scorm, attempts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last attempt (number and whether it's offline).
|
||||||
|
* It'll be the highest number as long as it doesn't surpass the max number of attempts.
|
||||||
|
*
|
||||||
|
* @param {any} scorm SCORM object.
|
||||||
|
* @param {AddonModScormAttemptCountResult} attempts Attempts count.
|
||||||
|
* @return {{number: number, offline: boolean}} Last attempt data.
|
||||||
|
*/
|
||||||
|
protected getLastBeforeMax(scorm: any, attempts: AddonModScormAttemptCountResult): {number: number, offline: boolean} {
|
||||||
|
if (scorm.maxattempt != 0 && attempts.lastAttempt.number > scorm.maxattempt) {
|
||||||
|
return {
|
||||||
|
number: scorm.maxattempt,
|
||||||
|
offline: attempts.offline.indexOf(scorm.maxattempt) > -1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
number: attempts.lastAttempt.number,
|
||||||
|
offline: attempts.lastAttempt.offline
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate
|
||||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||||
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||||
import { AddonModScormProvider } from './providers/scorm';
|
import { AddonModScormProvider } from './providers/scorm';
|
||||||
|
import { AddonModScormHelperProvider } from './providers/helper';
|
||||||
import { AddonModScormOfflineProvider } from './providers/scorm-offline';
|
import { AddonModScormOfflineProvider } from './providers/scorm-offline';
|
||||||
import { AddonModScormModuleHandler } from './providers/module-handler';
|
import { AddonModScormModuleHandler } from './providers/module-handler';
|
||||||
import { AddonModScormPrefetchHandler } from './providers/prefetch-handler';
|
import { AddonModScormPrefetchHandler } from './providers/prefetch-handler';
|
||||||
|
@ -36,6 +37,7 @@ import { AddonModScormComponentsModule } from './components/components.module';
|
||||||
providers: [
|
providers: [
|
||||||
AddonModScormProvider,
|
AddonModScormProvider,
|
||||||
AddonModScormOfflineProvider,
|
AddonModScormOfflineProvider,
|
||||||
|
AddonModScormHelperProvider,
|
||||||
AddonModScormSyncProvider,
|
AddonModScormSyncProvider,
|
||||||
AddonModScormModuleHandler,
|
AddonModScormModuleHandler,
|
||||||
AddonModScormPrefetchHandler,
|
AddonModScormPrefetchHandler,
|
||||||
|
|
|
@ -104,3 +104,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Different levels of padding.
|
||||||
|
@for $i from 0 through 15 {
|
||||||
|
.ios .core-padding-#{$i} {
|
||||||
|
padding-left: 15px * $i + $item-ios-padding-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -105,3 +105,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Different levels of padding.
|
||||||
|
@for $i from 0 through 15 {
|
||||||
|
.md .core-padding-#{$i} {
|
||||||
|
padding-left: 15px * $i + $item-md-padding-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -621,7 +621,7 @@ canvas[core-chart] {
|
||||||
color: $color-base;
|
color: $color-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-#{$color-name}, p.#{$color-name}, .item p.text-#{$color-name} {
|
.text-#{$color-name}, p.text-#{$color-name}, .item p.text-#{$color-name}, .card p.text-#{$color-name} {
|
||||||
color: $color-base;
|
color: $color-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,3 +40,10 @@
|
||||||
top: $navbar-wp-height;
|
top: $navbar-wp-height;
|
||||||
height: calc(100% - #{($navbar-wp-height)});
|
height: calc(100% - #{($navbar-wp-height)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Different levels of padding.
|
||||||
|
@for $i from 0 through 15 {
|
||||||
|
.wp .core-padding-#{$i} {
|
||||||
|
padding-left: 15px * $i + $item-wp-padding-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
protected courseProvider: CoreCourseProvider;
|
protected courseProvider: CoreCourseProvider;
|
||||||
protected appProvider: CoreAppProvider;
|
protected appProvider: CoreAppProvider;
|
||||||
protected eventsProvider: CoreEventsProvider;
|
protected eventsProvider: CoreEventsProvider;
|
||||||
protected modulePrefetchProvider: CoreCourseModulePrefetchDelegate;
|
protected modulePrefetchDelegate: CoreCourseModulePrefetchDelegate;
|
||||||
|
|
||||||
constructor(injector: Injector, protected content?: Content) {
|
constructor(injector: Injector, protected content?: Content) {
|
||||||
super(injector);
|
super(injector);
|
||||||
|
@ -55,6 +55,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
this.courseProvider = injector.get(CoreCourseProvider);
|
this.courseProvider = injector.get(CoreCourseProvider);
|
||||||
this.appProvider = injector.get(CoreAppProvider);
|
this.appProvider = injector.get(CoreAppProvider);
|
||||||
this.eventsProvider = injector.get(CoreEventsProvider);
|
this.eventsProvider = injector.get(CoreEventsProvider);
|
||||||
|
this.modulePrefetchDelegate = injector.get(CoreCourseModulePrefetchDelegate);
|
||||||
|
|
||||||
const network = injector.get(Network);
|
const network = injector.get(Network);
|
||||||
|
|
||||||
|
@ -158,7 +159,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.content && this.content.scrollToTop();
|
this.content && this.content.scrollToTop();
|
||||||
|
|
||||||
return this.refreshContent(true, showErrors);
|
return this.refreshContent(sync, showErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,7 +227,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
}, this.siteId);
|
}, this.siteId);
|
||||||
|
|
||||||
// Also, get the current status.
|
// Also, get the current status.
|
||||||
this.modulePrefetchProvider.getModuleStatus(this.module, this.courseId).then((status) => {
|
this.modulePrefetchDelegate.getModuleStatus(this.module, this.courseId).then((status) => {
|
||||||
this.currentStatus = status;
|
this.currentStatus = status;
|
||||||
this.showStatus(status);
|
this.showStatus(status);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue