diff --git a/src/addon/mod/quiz/components/index/index.ts b/src/addon/mod/quiz/components/index/index.ts index 2e60facfa..d43378381 100644 --- a/src/addon/mod/quiz/components/index/index.ts +++ b/src/addon/mod/quiz/components/index/index.ts @@ -16,7 +16,6 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content, NavController } from 'ionic-angular'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; -import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { AddonModQuizProvider } from '../../providers/quiz'; import { AddonModQuizHelperProvider } from '../../providers/helper'; import { AddonModQuizOfflineProvider } from '../../providers/quiz-offline'; @@ -71,8 +70,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() content: Content, protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider, protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate, - protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController, - protected prefetchDelegate: CoreCourseModulePrefetchDelegate) { + protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController) { 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. const isDownloaded = this.currentStatus == CoreConstants.DOWNLOADED; - if (!isDownloaded || !this.prefetchDelegate.canCheckUpdates()) { + if (!isDownloaded || !this.modulePrefetchDelegate.canCheckUpdates()) { // Prefetch the quiz. this.showStatusSpinner = true; @@ -125,7 +123,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp // Success downloading, open quiz. this.openQuiz(); }).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. // If the site doesn't support check updates, continue too because we cannot tell if there's something new. this.openQuiz(); diff --git a/src/addon/mod/scorm/components/components.module.ts b/src/addon/mod/scorm/components/components.module.ts new file mode 100644 index 000000000..2a9c6569b --- /dev/null +++ b/src/addon/mod/scorm/components/components.module.ts @@ -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 {} diff --git a/src/addon/mod/scorm/components/index/index.html b/src/addon/mod/scorm/components/index/index.html new file mode 100644 index 000000000..a9bb0b3d0 --- /dev/null +++ b/src/addon/mod/scorm/components/index/index.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + +
+ + {{ scorm.warningMessage }} +
+ +
+ + + +

{{ 'addon.mod_scorm.attempts' | translate }}

+
+ + + +

{{ 'addon.mod_scorm.noattemptsallowed' | translate }}

+

{{ 'core.unlimited' | translate }}

+

{{ scorm.maxattempt }}

+
+ +

{{ 'addon.mod_scorm.noattemptsmade' | translate }}

+

{{ scorm.numAttempts }}

+
+ +

{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}

+

{{ attempt.grade }}

+

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

+
+
+ +

{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}

+

{{ attempt.grade }}

+

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

+

{{ 'addon.mod_scorm.offlineattemptnote' | translate }}

+

{{ 'addon.mod_scorm.offlineattemptovermax' | translate }}

+
+ +

{{ 'addon.mod_scorm.grademethod' | translate }}

+

{{ scorm.gradeMethodReadable }}

+
+ +

{{ 'addon.mod_scorm.gradereported' | translate }}

+

{{ scorm.grade }}

+

{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}

+
+ +

{{ 'core.lastsync' | translate }}

+

{{ syncTime }}

+
+
+
+ + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ + + + +

{{ 'addon.mod_scorm.contents' | translate }}

+
+ + + {{ 'addon.mod_scorm.organizations' | translate }} + + {{ org.title }} + + + + + + + +

{{ 'addon.mod_scorm.dataattemptshown' | translate:{number: attemptToContinue} }}

+

{{ currentOrganization.title }}

+
+

+ + {{ sco.title }} + {{ sco.title }} +

+
+
+
+
+ + + + +

{{ errorMessage | translate }}

+
+ + + {{ 'core.openinbrowser' | translate }} + + + +
+ + + + +

{{ 'addon.mod_scorm.exceededmaxattempts' | translate }}

+
+
+ + + + + +
+ +

{{ 'addon.mod_scorm.mode' | translate }}

+
+ + {{ 'addon.mod_scorm.browse' | translate }} + + + + {{ 'addon.mod_scorm.normal' | translate }} + + +
+ + + + {{ 'addon.mod_scorm.newattempt' | translate }} + + + + + + + +

{{ statusMessage | translate }}

+
+ + {{ 'addon.mod_scorm.enter' | translate }} + +
+ + + + +

{{ progressMessage | translate }}

+

{{ 'core.percentagenumber' | translate:{$a: percentage} }}

+
+
+
+
+
diff --git a/src/addon/mod/scorm/components/index/index.scss b/src/addon/mod/scorm/components/index/index.scss new file mode 100644 index 000000000..9cfb94875 --- /dev/null +++ b/src/addon/mod/scorm/components/index/index.scss @@ -0,0 +1,9 @@ +addon-mod-scorm-index { + + .addon-mod_scorm-toc { + img { + width: auto; + display: inline; + } + } +} diff --git a/src/addon/mod/scorm/components/index/index.ts b/src/addon/mod/scorm/components/index/index.ts new file mode 100644 index 000000000..6a18cc836 --- /dev/null +++ b/src/addon/mod/scorm/components/index/index.ts @@ -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} Promise resolved when done. + */ + protected downloadScormPackage(): Promise { + 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} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + + // 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} Promise resolved when done. + */ + protected fetchStructure(): Promise { + 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} Promise resolved when done. + */ + protected getAttemptGrade(attempt: number, offline: boolean, attempts: any): Promise { + 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} Promise resolved when done. + */ + protected getReportedGrades(): Promise { + 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} Resolved when done. + */ + protected invalidateContent(): Promise { + 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} Promise resolved when done. + */ + protected loadOrganizationToc(organizationId: string): Promise { + 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} Promise resolved when done. + */ + protected sync(): Promise { + return this.scormSync.syncScorm(this.scorm); + } +} diff --git a/src/addon/mod/scorm/pages/index/index.html b/src/addon/mod/scorm/pages/index/index.html new file mode 100644 index 000000000..62099e9bf --- /dev/null +++ b/src/addon/mod/scorm/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/scorm/pages/index/index.module.ts b/src/addon/mod/scorm/pages/index/index.module.ts new file mode 100644 index 000000000..68bd2e47b --- /dev/null +++ b/src/addon/mod/scorm/pages/index/index.module.ts @@ -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 {} diff --git a/src/addon/mod/scorm/pages/index/index.ts b/src/addon/mod/scorm/pages/index/index.ts new file mode 100644 index 000000000..9179fc6d1 --- /dev/null +++ b/src/addon/mod/scorm/pages/index/index.ts @@ -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(); + } +} diff --git a/src/addon/mod/scorm/providers/helper.ts b/src/addon/mod/scorm/providers/helper.ts new file mode 100644 index 000000000..5c0849fee --- /dev/null +++ b/src/addon/mod/scorm/providers/helper.ts @@ -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} Promise resolved if the user confirms or no confirmation needed. + */ + confirmDownload(scorm: any, isOutdated?: boolean): Promise { + // 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 + }; + } + } +} diff --git a/src/addon/mod/scorm/scorm.module.ts b/src/addon/mod/scorm/scorm.module.ts index 2d9e6c6b3..ea48f5699 100644 --- a/src/addon/mod/scorm/scorm.module.ts +++ b/src/addon/mod/scorm/scorm.module.ts @@ -18,6 +18,7 @@ import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { AddonModScormProvider } from './providers/scorm'; +import { AddonModScormHelperProvider } from './providers/helper'; import { AddonModScormOfflineProvider } from './providers/scorm-offline'; import { AddonModScormModuleHandler } from './providers/module-handler'; import { AddonModScormPrefetchHandler } from './providers/prefetch-handler'; @@ -36,6 +37,7 @@ import { AddonModScormComponentsModule } from './components/components.module'; providers: [ AddonModScormProvider, AddonModScormOfflineProvider, + AddonModScormHelperProvider, AddonModScormSyncProvider, AddonModScormModuleHandler, AddonModScormPrefetchHandler, diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index 6ec16b1d5..a7410863d 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -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; + } +} diff --git a/src/app/app.md.scss b/src/app/app.md.scss index f96fff139..866b98788 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -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; + } +} diff --git a/src/app/app.scss b/src/app/app.scss index 624b27ebd..efc43f114 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -621,7 +621,7 @@ canvas[core-chart] { 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; } } diff --git a/src/app/app.wp.scss b/src/app/app.wp.scss index 431dfdb08..0e7342032 100644 --- a/src/app/app.wp.scss +++ b/src/app/app.wp.scss @@ -40,3 +40,10 @@ top: $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; + } +} diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 875fec056..f1fe1bf30 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -46,7 +46,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR protected courseProvider: CoreCourseProvider; protected appProvider: CoreAppProvider; protected eventsProvider: CoreEventsProvider; - protected modulePrefetchProvider: CoreCourseModulePrefetchDelegate; + protected modulePrefetchDelegate: CoreCourseModulePrefetchDelegate; constructor(injector: Injector, protected content?: Content) { super(injector); @@ -55,6 +55,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.courseProvider = injector.get(CoreCourseProvider); this.appProvider = injector.get(CoreAppProvider); this.eventsProvider = injector.get(CoreEventsProvider); + this.modulePrefetchDelegate = injector.get(CoreCourseModulePrefetchDelegate); const network = injector.get(Network); @@ -158,7 +159,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.loaded = false; 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); // 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.showStatus(status); });