commit
badb71cb8e
|
@ -684,6 +684,7 @@
|
||||||
"addon.mod_h5pactivity.no_compatible_track": "h5pactivity",
|
"addon.mod_h5pactivity.no_compatible_track": "h5pactivity",
|
||||||
"addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp",
|
"addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp",
|
||||||
"addon.mod_h5pactivity.outcome": "h5pactivity",
|
"addon.mod_h5pactivity.outcome": "h5pactivity",
|
||||||
|
"addon.mod_h5pactivity.previewmode": "h5pactivity",
|
||||||
"addon.mod_h5pactivity.result_fill-in": "h5pactivity",
|
"addon.mod_h5pactivity.result_fill-in": "h5pactivity",
|
||||||
"addon.mod_h5pactivity.result_other": "h5pactivity",
|
"addon.mod_h5pactivity.result_other": "h5pactivity",
|
||||||
"addon.mod_h5pactivity.review_my_attempts": "h5pactivity",
|
"addon.mod_h5pactivity.review_my_attempts": "h5pactivity",
|
||||||
|
@ -1405,6 +1406,7 @@
|
||||||
"core.confirmdeletefile": "repository",
|
"core.confirmdeletefile": "repository",
|
||||||
"core.confirmgotabroot": "local_moodlemobileapp",
|
"core.confirmgotabroot": "local_moodlemobileapp",
|
||||||
"core.confirmgotabrootdefault": "local_moodlemobileapp",
|
"core.confirmgotabrootdefault": "local_moodlemobileapp",
|
||||||
|
"core.confirmleaveunknownchanges": "local_moodlemobileapp",
|
||||||
"core.confirmloss": "local_moodlemobileapp",
|
"core.confirmloss": "local_moodlemobileapp",
|
||||||
"core.confirmopeninbrowser": "local_moodlemobileapp",
|
"core.confirmopeninbrowser": "local_moodlemobileapp",
|
||||||
"core.considereddigitalminor": "moodle",
|
"core.considereddigitalminor": "moodle",
|
||||||
|
@ -1858,6 +1860,7 @@
|
||||||
"core.mod_folder": "folder/pluginname",
|
"core.mod_folder": "folder/pluginname",
|
||||||
"core.mod_forum": "forum/pluginname",
|
"core.mod_forum": "forum/pluginname",
|
||||||
"core.mod_glossary": "glossary/pluginname",
|
"core.mod_glossary": "glossary/pluginname",
|
||||||
|
"core.mod_h5pactivity": "h5pactivity/pluginname",
|
||||||
"core.mod_ims": "imscp/pluginname",
|
"core.mod_ims": "imscp/pluginname",
|
||||||
"core.mod_imscp": "imscp/pluginname",
|
"core.mod_imscp": "imscp/pluginname",
|
||||||
"core.mod_label": "label/pluginname",
|
"core.mod_label": "label/pluginname",
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
<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="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="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
|
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="loaded && 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]="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($event)" [iconAction]="prefetchStatusIcon" [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.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
|
@ -16,11 +17,21 @@
|
||||||
|
|
||||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>
|
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>
|
||||||
|
|
||||||
|
<!-- Offline data stored. -->
|
||||||
|
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
|
||||||
|
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
<!-- Offline disabled. -->
|
<!-- Offline disabled. -->
|
||||||
<ion-card class="core-warning-card" icon-start *ngIf="!siteCanDownload && playing">
|
<ion-card class="core-warning-card" icon-start *ngIf="!siteCanDownload && playing">
|
||||||
<ion-icon name="warning"></ion-icon> {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
|
<ion-icon name="warning"></ion-icon> {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Preview mode. -->
|
||||||
|
<ion-card class="core-warning-card" icon-start *ngIf="accessInfo && !trackComponent">
|
||||||
|
<ion-icon name="warning"></ion-icon> {{ 'addon.mod_h5pactivity.previewmode' | translate }}
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
<ion-list *ngIf="deployedFile && !playing">
|
<ion-list *ngIf="deployedFile && !playing">
|
||||||
<ion-item text-wrap *ngIf="stateMessage">
|
<ion-item text-wrap *ngIf="stateMessage">
|
||||||
<p >{{ stateMessage | translate }}</p>
|
<p >{{ stateMessage | translate }}</p>
|
||||||
|
@ -39,5 +50,5 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"></core-h5p-iframe>
|
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl" [trackComponent]="trackComponent" [contextId]="h5pActivity.context"></core-h5p-iframe>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -24,12 +24,15 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main
|
||||||
import { CoreH5P } from '@core/h5p/providers/h5p';
|
import { CoreH5P } from '@core/h5p/providers/h5p';
|
||||||
import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
|
import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
|
||||||
import { CoreH5PHelper } from '@core/h5p/classes/helper';
|
import { CoreH5PHelper } from '@core/h5p/classes/helper';
|
||||||
|
import { CoreXAPI } from '@core/xapi/providers/xapi';
|
||||||
|
import { CoreXAPIOffline } from '@core/xapi/providers/offline';
|
||||||
import { CoreConstants } from '@core/constants';
|
import { CoreConstants } from '@core/constants';
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo
|
AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo
|
||||||
} from '../../providers/h5pactivity';
|
} from '../../providers/h5pactivity';
|
||||||
|
import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays an H5P activity entry page.
|
* Component that displays an H5P activity entry page.
|
||||||
|
@ -57,10 +60,15 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
fileUrl: string; // The fileUrl to use to play the package.
|
fileUrl: string; // The fileUrl to use to play the package.
|
||||||
state: string; // State of the file.
|
state: string; // State of the file.
|
||||||
siteCanDownload: boolean;
|
siteCanDownload: boolean;
|
||||||
|
trackComponent: string; // Component for tracking.
|
||||||
|
hasOffline: boolean;
|
||||||
|
isOpeningPage: boolean;
|
||||||
|
|
||||||
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
|
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
|
||||||
|
protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
|
||||||
protected site: CoreSite;
|
protected site: CoreSite;
|
||||||
protected observer;
|
protected observer;
|
||||||
|
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
|
||||||
|
|
||||||
constructor(injector: Injector,
|
constructor(injector: Injector,
|
||||||
@Optional() protected content: Content) {
|
@Optional() protected content: Content) {
|
||||||
|
@ -68,6 +76,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
|
|
||||||
this.site = this.sitesProvider.getCurrentSite();
|
this.site = this.sitesProvider.getCurrentSite();
|
||||||
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
|
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
|
||||||
|
|
||||||
|
// Listen for messages from the iframe.
|
||||||
|
this.messageListenerFunction = this.onIframeMessage.bind(this);
|
||||||
|
window.addEventListener('message', this.messageListenerFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,23 +108,30 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id);
|
this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId);
|
||||||
|
|
||||||
this.dataRetrieved.emit(this.h5pActivity);
|
this.dataRetrieved.emit(this.h5pActivity);
|
||||||
this.description = this.h5pActivity.intro;
|
this.description = this.h5pActivity.intro;
|
||||||
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
|
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
|
||||||
|
|
||||||
if (this.h5pActivity.package && this.h5pActivity.package[0]) {
|
if (sync) {
|
||||||
// The online player should use the original file, not the trusted one.
|
await this.syncActivity(showErrors);
|
||||||
this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
|
|
||||||
this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
this.checkHasOffline(),
|
||||||
this.fetchAccessInfo(),
|
this.fetchAccessInfo(),
|
||||||
this.fetchDeployedFileData(),
|
this.fetchDeployedFileData(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
this.trackComponent = this.accessInfo.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : '';
|
||||||
|
|
||||||
|
if (this.h5pActivity.package && this.h5pActivity.package[0]) {
|
||||||
|
// The online player should use the original file, not the trusted one.
|
||||||
|
this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
|
||||||
|
this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions, this.trackComponent);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) {
|
if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) {
|
||||||
// Cannot download the file or already downloaded, play the package directly.
|
// Cannot download the file or already downloaded, play the package directly.
|
||||||
this.play();
|
this.play();
|
||||||
|
@ -127,13 +146,22 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the access info and store it in the right variables.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async checkHasOffline(): Promise<void> {
|
||||||
|
this.hasOffline = await CoreXAPIOffline.instance.contextHasStatements(this.h5pActivity.context, this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the access info and store it in the right variables.
|
* Fetch the access info and store it in the right variables.
|
||||||
*
|
*
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async fetchAccessInfo(): Promise<void> {
|
protected async fetchAccessInfo(): Promise<void> {
|
||||||
this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id);
|
this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -322,8 +350,114 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
/**
|
/**
|
||||||
* Go to view user events.
|
* Go to view user events.
|
||||||
*/
|
*/
|
||||||
viewMyAttempts(): void {
|
async viewMyAttempts(): Promise<void> {
|
||||||
this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id});
|
this.isOpeningPage = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {
|
||||||
|
courseId: this.courseId,
|
||||||
|
h5pActivityId: this.h5pActivity.id,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.isOpeningPage = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Treat an iframe message event.
|
||||||
|
*
|
||||||
|
* @param event Event.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async onIframeMessage(event: MessageEvent): Promise<void> {
|
||||||
|
if (!event.data || !CoreXAPI.instance.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
offline: this.hasOffline,
|
||||||
|
courseId: this.courseId,
|
||||||
|
extra: this.h5pActivity.name,
|
||||||
|
siteId: this.site.getId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sent = await CoreXAPI.instance.postStatements(this.h5pActivity.context, event.data.component,
|
||||||
|
JSON.stringify(event.data.statements), options);
|
||||||
|
|
||||||
|
this.hasOffline = !sent;
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
try {
|
||||||
|
// Invalidate attempts.
|
||||||
|
await AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivity.id, undefined, this.siteId);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an event is an XAPI post statement of the current activity.
|
||||||
|
*
|
||||||
|
* @param data Event data.
|
||||||
|
* @return Whether it's an XAPI post statement of the current activity.
|
||||||
|
*/
|
||||||
|
protected isCurrentXAPIPost(data: any): boolean {
|
||||||
|
if (data.context != 'moodleapp' || data.action != 'xapi_post_statement' || !data.statements) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the event belongs to this activity.
|
||||||
|
const trackingUrl = data.statements[0] && data.statements[0].object && data.statements[0].object.id;
|
||||||
|
if (!trackingUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.site.containsUrl(trackingUrl)) {
|
||||||
|
// The event belongs to another site, weird scenario. Maybe some JS running in background.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = trackingUrl.match(/xapi\/activity\/(\d+)/);
|
||||||
|
|
||||||
|
return match && match[1] == this.h5pActivity.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the sync of the activity.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected sync(): Promise<any> {
|
||||||
|
return AddonModH5PActivitySync.instance.syncActivity(this.h5pActivity.context, this.site.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autosync event has been received.
|
||||||
|
*
|
||||||
|
* @param syncEventData Data receiven on sync observer.
|
||||||
|
*/
|
||||||
|
protected autoSyncEventReceived(syncEventData: any): void {
|
||||||
|
this.checkHasOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to blog posts.
|
||||||
|
*
|
||||||
|
* @param event Event.
|
||||||
|
*/
|
||||||
|
async gotoBlog(event: any): Promise<void> {
|
||||||
|
this.isOpeningPage = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await super.gotoBlog(event);
|
||||||
|
} finally {
|
||||||
|
this.isOpeningPage = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -331,5 +465,6 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.observer && this.observer.off();
|
this.observer && this.observer.off();
|
||||||
|
window.removeEventListener('message', this.messageListenerFunction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreCronDelegate } from '@providers/cron';
|
||||||
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||||
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
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';
|
||||||
|
@ -21,13 +22,16 @@ import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-
|
||||||
import { AddonModH5PActivityComponentsModule } from './components/components.module';
|
import { AddonModH5PActivityComponentsModule } from './components/components.module';
|
||||||
import { AddonModH5PActivityModuleHandler } from './providers/module-handler';
|
import { AddonModH5PActivityModuleHandler } from './providers/module-handler';
|
||||||
import { AddonModH5PActivityProvider } from './providers/h5pactivity';
|
import { AddonModH5PActivityProvider } from './providers/h5pactivity';
|
||||||
|
import { AddonModH5PActivitySyncProvider } from './providers/sync';
|
||||||
import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler';
|
import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler';
|
||||||
import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler';
|
import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler';
|
||||||
import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler';
|
import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler';
|
||||||
|
import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler';
|
||||||
|
|
||||||
// List of providers (without handlers).
|
// List of providers (without handlers).
|
||||||
export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
|
export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
|
||||||
AddonModH5PActivityProvider,
|
AddonModH5PActivityProvider,
|
||||||
|
AddonModH5PActivitySyncProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -38,10 +42,12 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AddonModH5PActivityProvider,
|
AddonModH5PActivityProvider,
|
||||||
|
AddonModH5PActivitySyncProvider,
|
||||||
AddonModH5PActivityModuleHandler,
|
AddonModH5PActivityModuleHandler,
|
||||||
AddonModH5PActivityPrefetchHandler,
|
AddonModH5PActivityPrefetchHandler,
|
||||||
AddonModH5PActivityIndexLinkHandler,
|
AddonModH5PActivityIndexLinkHandler,
|
||||||
AddonModH5PActivityReportLinkHandler,
|
AddonModH5PActivityReportLinkHandler,
|
||||||
|
AddonModH5PActivitySyncCronHandler,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AddonModH5PActivityModule {
|
export class AddonModH5PActivityModule {
|
||||||
|
@ -51,11 +57,14 @@ export class AddonModH5PActivityModule {
|
||||||
prefetchHandler: AddonModH5PActivityPrefetchHandler,
|
prefetchHandler: AddonModH5PActivityPrefetchHandler,
|
||||||
linksDelegate: CoreContentLinksDelegate,
|
linksDelegate: CoreContentLinksDelegate,
|
||||||
indexHandler: AddonModH5PActivityIndexLinkHandler,
|
indexHandler: AddonModH5PActivityIndexLinkHandler,
|
||||||
reportLinkHandler: AddonModH5PActivityReportLinkHandler) {
|
reportLinkHandler: AddonModH5PActivityReportLinkHandler,
|
||||||
|
cronDelegate: CoreCronDelegate,
|
||||||
|
syncHandler: AddonModH5PActivitySyncCronHandler) {
|
||||||
|
|
||||||
moduleDelegate.registerHandler(moduleHandler);
|
moduleDelegate.registerHandler(moduleHandler);
|
||||||
prefetchDelegate.registerHandler(prefetchHandler);
|
prefetchDelegate.registerHandler(prefetchHandler);
|
||||||
linksDelegate.registerHandler(indexHandler);
|
linksDelegate.registerHandler(indexHandler);
|
||||||
linksDelegate.registerHandler(reportLinkHandler);
|
linksDelegate.registerHandler(reportLinkHandler);
|
||||||
|
cronDelegate.register(syncHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
|
"no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
|
||||||
"offlinedisabledwarning": "You will need to be online to view the H5P package.",
|
"offlinedisabledwarning": "You will need to be online to view the H5P package.",
|
||||||
"outcome": "Outcome",
|
"outcome": "Outcome",
|
||||||
|
"previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
|
||||||
"result_fill-in": "Fill-in text",
|
"result_fill-in": "Fill-in text",
|
||||||
"result_other": "Unkown interaction type",
|
"result_other": "Unkown interaction type",
|
||||||
"review_my_attempts": "View my attempts",
|
"review_my_attempts": "View my attempts",
|
||||||
|
|
|
@ -14,9 +14,12 @@
|
||||||
|
|
||||||
import { Component, ViewChild } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import { IonicPage, NavParams } from 'ionic-angular';
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { CoreDomUtils } from '@providers/utils/dom';
|
||||||
import { AddonModH5PActivityIndexComponent } from '../../components/index/index';
|
import { AddonModH5PActivityIndexComponent } from '../../components/index/index';
|
||||||
import { AddonModH5PActivityData } from '../../providers/h5pactivity';
|
import { AddonModH5PActivityData } from '../../providers/h5pactivity';
|
||||||
|
|
||||||
|
import { Translate } from '@singletons/core.singletons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays an H5P activity.
|
* Page that displays an H5P activity.
|
||||||
*/
|
*/
|
||||||
|
@ -46,4 +49,17 @@ export class AddonModH5PActivityIndexPage {
|
||||||
updateData(h5p: AddonModH5PActivityData): void {
|
updateData(h5p: AddonModH5PActivityData): void {
|
||||||
this.title = h5p.name || this.title;
|
this.title = h5p.name || this.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we can leave the page or not.
|
||||||
|
*
|
||||||
|
* @return Resolved if we can leave it, rejected if not.
|
||||||
|
*/
|
||||||
|
ionViewCanLeave(): Promise<void> {
|
||||||
|
if (!this.h5pComponent.playing || this.h5pComponent.isOpeningPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmleaveunknownchanges'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AddonModH5PActivityProvider {
|
export class AddonModH5PActivityProvider {
|
||||||
static COMPONENT = 'mmaModH5PActivity';
|
static COMPONENT = 'mmaModH5PActivity';
|
||||||
|
static TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
|
||||||
|
|
||||||
protected ROOT_CACHE_KEY = 'mmaModH5PActivity:';
|
protected ROOT_CACHE_KEY = 'mmaModH5PActivity:';
|
||||||
|
|
||||||
|
@ -384,6 +385,20 @@ export class AddonModH5PActivityProvider {
|
||||||
return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId);
|
return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an H5P activity by context ID.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param forceCache Whether it should always return cached data.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the activity data.
|
||||||
|
*/
|
||||||
|
getH5PActivityByContextId(courseId: number, contextId: number, forceCache?: boolean, siteId?: string)
|
||||||
|
: Promise<AddonModH5PActivityData> {
|
||||||
|
return this.getH5PActivityByField(courseId, 'context', contextId, forceCache, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an H5P activity by instance ID.
|
* Get an H5P activity by instance ID.
|
||||||
*
|
*
|
||||||
|
@ -595,6 +610,7 @@ export type AddonModH5PActivityData = {
|
||||||
grademethod: number; // Which H5P attempt is used for grading.
|
grademethod: number; // Which H5P attempt is used for grading.
|
||||||
contenthash?: string; // Sha1 hash of file content.
|
contenthash?: string; // Sha1 hash of file content.
|
||||||
coursemodule: number; // Coursemodule.
|
coursemodule: number; // Coursemodule.
|
||||||
|
context: number; // Context ID.
|
||||||
introfiles: CoreWSExternalFile[];
|
introfiles: CoreWSExternalFile[];
|
||||||
package: CoreWSExternalFile[];
|
package: CoreWSExternalFile[];
|
||||||
deployedfile?: {
|
deployedfile?: {
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreCronHandler } from '@providers/cron';
|
||||||
|
import { AddonModH5PActivitySync } from './sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization cron handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModH5PActivitySyncCronHandler implements CoreCronHandler {
|
||||||
|
name = 'AddonModH5PActivitySyncCronHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param siteId ID of the site affected, undefined for all sites.
|
||||||
|
* @param force Wether the execution is forced (manual sync).
|
||||||
|
* @return Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
execute(siteId?: string, force?: boolean): Promise<any> {
|
||||||
|
return AddonModH5PActivitySync.instance.syncAllActivities(siteId, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return AddonModH5PActivitySync.instance.syncInterval;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
import { CoreEvents } from '@providers/events';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
import { CoreUtils } from '@providers/utils/utils';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
|
import { CoreCourse } from '@core/course/providers/course';
|
||||||
|
import { CoreCourseLogHelper } from '@core/course/providers/log-helper';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||||
|
import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync';
|
||||||
|
import { CoreXAPI } from '@core/xapi/providers/xapi';
|
||||||
|
import { CoreXAPIOffline } from '@core/xapi/providers/offline';
|
||||||
|
import { AddonModH5PActivity, AddonModH5PActivityProvider } from './h5pactivity';
|
||||||
|
import { AddonModH5PActivityPrefetchHandler } from './prefetch-handler';
|
||||||
|
|
||||||
|
import { makeSingleton } from '@singletons/core.singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to sync H5P activities.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseProvider {
|
||||||
|
|
||||||
|
static AUTO_SYNCED = 'addon_mod_h5pactivity_autom_synced';
|
||||||
|
protected componentTranslate: string;
|
||||||
|
|
||||||
|
constructor(sitesProvider: CoreSitesProvider,
|
||||||
|
loggerProvider: CoreLoggerProvider,
|
||||||
|
appProvider: CoreAppProvider,
|
||||||
|
translate: TranslateService,
|
||||||
|
textUtils: CoreTextUtilsProvider,
|
||||||
|
syncProvider: CoreSyncProvider,
|
||||||
|
timeUtils: CoreTimeUtilsProvider,
|
||||||
|
prefetchHandler: AddonModH5PActivityPrefetchHandler,
|
||||||
|
prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
||||||
|
|
||||||
|
super('AddonModH5PActivitySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate,
|
||||||
|
timeUtils, prefetchDelegate, prefetchHandler);
|
||||||
|
|
||||||
|
this.componentTranslate = CoreCourse.instance.translateModuleName('h5pactivity');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to synchronize all the H5P activities in a certain site or in all sites.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @param force Wether to force sync not depending on last execution.
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
syncAllActivities(siteId?: string, force?: boolean): Promise<void> {
|
||||||
|
return this.syncOnSites('H5P activities', this.syncAllActivitiesFunc.bind(this), [force], siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all H5P activities on a site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @param force Wether to force sync not depending on last execution.
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected async syncAllActivitiesFunc(siteId?: string, force?: boolean): Promise<void> {
|
||||||
|
const entries = await CoreXAPIOffline.instance.getAllStatements(siteId);
|
||||||
|
|
||||||
|
// Sync all responses.
|
||||||
|
const promises = entries.map((response) => {
|
||||||
|
const promise = force ? this.syncActivity(response.contextid, siteId) :
|
||||||
|
this.syncActivityIfNeeded(response.contextid, siteId);
|
||||||
|
|
||||||
|
return promise.then((result) => {
|
||||||
|
if (result && result.updated) {
|
||||||
|
// Sync successful, send event.
|
||||||
|
CoreEvents.instance.trigger(AddonModH5PActivitySyncProvider.AUTO_SYNCED, {
|
||||||
|
contextId: response.contextid,
|
||||||
|
warnings: result.warnings,
|
||||||
|
}, siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync an H5P activity only if a certain time has passed since the last time.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID of the activity.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the activity is synced or it doesn't need to be synced.
|
||||||
|
*/
|
||||||
|
async syncActivityIfNeeded(contextId: number, siteId?: string): Promise<any> {
|
||||||
|
const needed = await this.isSyncNeeded(contextId, siteId);
|
||||||
|
|
||||||
|
if (needed) {
|
||||||
|
return this.syncActivity(contextId, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize an H5P activity. If it's already being synced it will reuse the same promise.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID of the activity.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
syncActivity(contextId: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (!this.appProvider.isOnline()) {
|
||||||
|
// Cannot sync in offline.
|
||||||
|
throw this.translate.instant('core.networkerrormsg');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSyncing(contextId, siteId)) {
|
||||||
|
// There's already a sync ongoing for this discussion, return the promise.
|
||||||
|
return this.getOngoingSync(contextId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addOngoingSync(contextId, this.syncActivityData(contextId, siteId), siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize an H5P activity.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID of the activity.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected async syncActivityData(contextId: number, siteId: string): Promise<{warnings: string[], updated: boolean}> {
|
||||||
|
|
||||||
|
this.logger.debug(`Try to sync H5P activity with context ID '${contextId}'`);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
warnings: [],
|
||||||
|
updated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all the statements stored for the activity.
|
||||||
|
const entries = await CoreXAPIOffline.instance.getContextStatements(contextId, siteId);
|
||||||
|
|
||||||
|
if (!entries || !entries.length) {
|
||||||
|
// Nothing to sync.
|
||||||
|
await this.setSyncTime(contextId, siteId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the activity instance.
|
||||||
|
const courseId = entries[0].courseid;
|
||||||
|
|
||||||
|
const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, false, siteId);
|
||||||
|
|
||||||
|
// Sync offline logs.
|
||||||
|
try {
|
||||||
|
await CoreCourseLogHelper.instance.syncIfNeeded(AddonModH5PActivityProvider.COMPONENT, h5pActivity.id, siteId);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the statements in order.
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const entry = entries[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreXAPI.instance.postStatementsOnline(entry.component, entry.statements, siteId);
|
||||||
|
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId);
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.instance.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means that statements cannot be submitted. Delete them.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId);
|
||||||
|
|
||||||
|
// Responses deleted, add a warning.
|
||||||
|
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: entry.extra,
|
||||||
|
error: this.textUtils.getErrorMessageFromError(error),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Stop synchronizing.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.updated) {
|
||||||
|
try {
|
||||||
|
// Data has been sent to server, invalidate attempts.
|
||||||
|
await AddonModH5PActivity.instance.invalidateUserAttempts(h5pActivity.id, undefined, siteId);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync finished, set sync time.
|
||||||
|
await this.setSyncTime(contextId, siteId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddonModH5PActivitySync extends makeSingleton(AddonModH5PActivitySyncProvider) {}
|
|
@ -88,6 +88,7 @@ import { CoreFilterModule } from '@core/filter/filter.module';
|
||||||
import { CoreH5PModule } from '@core/h5p/h5p.module';
|
import { CoreH5PModule } from '@core/h5p/h5p.module';
|
||||||
import { CoreSearchModule } from '@core/search/search.module';
|
import { CoreSearchModule } from '@core/search/search.module';
|
||||||
import { CoreEditorModule } from '@core/editor/editor.module';
|
import { CoreEditorModule } from '@core/editor/editor.module';
|
||||||
|
import { CoreXAPIModule } from '@core/xapi/xapi.module';
|
||||||
|
|
||||||
// Addon modules.
|
// Addon modules.
|
||||||
import { AddonBadgesModule } from '@addon/badges/badges.module';
|
import { AddonBadgesModule } from '@addon/badges/badges.module';
|
||||||
|
@ -241,6 +242,7 @@ export const WP_PROVIDER: any = null;
|
||||||
CoreH5PModule,
|
CoreH5PModule,
|
||||||
CoreSearchModule,
|
CoreSearchModule,
|
||||||
CoreEditorModule,
|
CoreEditorModule,
|
||||||
|
CoreXAPIModule,
|
||||||
AddonBadgesModule,
|
AddonBadgesModule,
|
||||||
AddonBlogModule,
|
AddonBlogModule,
|
||||||
AddonCalendarModule,
|
AddonCalendarModule,
|
||||||
|
|
|
@ -684,6 +684,7 @@
|
||||||
"addon.mod_h5pactivity.no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
|
"addon.mod_h5pactivity.no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
|
||||||
"addon.mod_h5pactivity.offlinedisabledwarning": "You will need to be online to view the H5P package.",
|
"addon.mod_h5pactivity.offlinedisabledwarning": "You will need to be online to view the H5P package.",
|
||||||
"addon.mod_h5pactivity.outcome": "Outcome",
|
"addon.mod_h5pactivity.outcome": "Outcome",
|
||||||
|
"addon.mod_h5pactivity.previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
|
||||||
"addon.mod_h5pactivity.result_fill-in": "Fill-in text",
|
"addon.mod_h5pactivity.result_fill-in": "Fill-in text",
|
||||||
"addon.mod_h5pactivity.result_other": "Unkown interaction type",
|
"addon.mod_h5pactivity.result_other": "Unkown interaction type",
|
||||||
"addon.mod_h5pactivity.review_my_attempts": "View my attempts",
|
"addon.mod_h5pactivity.review_my_attempts": "View my attempts",
|
||||||
|
@ -1406,6 +1407,7 @@
|
||||||
"core.confirmdeletefile": "Are you sure you want to delete this file?",
|
"core.confirmdeletefile": "Are you sure you want to delete this file?",
|
||||||
"core.confirmgotabroot": "Are you sure you want to go back to {{name}}?",
|
"core.confirmgotabroot": "Are you sure you want to go back to {{name}}?",
|
||||||
"core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
|
"core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
|
||||||
|
"core.confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have some unsaved changes they will be lost.",
|
||||||
"core.confirmloss": "Are you sure? All changes will be lost.",
|
"core.confirmloss": "Are you sure? All changes will be lost.",
|
||||||
"core.confirmopeninbrowser": "Do you want to open it in a web browser?",
|
"core.confirmopeninbrowser": "Do you want to open it in a web browser?",
|
||||||
"core.considereddigitalminor": "You are too young to create an account on this site.",
|
"core.considereddigitalminor": "You are too young to create an account on this site.",
|
||||||
|
@ -1859,6 +1861,7 @@
|
||||||
"core.mod_folder": "Folder",
|
"core.mod_folder": "Folder",
|
||||||
"core.mod_forum": "Forum",
|
"core.mod_forum": "Forum",
|
||||||
"core.mod_glossary": "Glossary",
|
"core.mod_glossary": "Glossary",
|
||||||
|
"core.mod_h5pactivity": "H5P",
|
||||||
"core.mod_ims": "IMS content package",
|
"core.mod_ims": "IMS content package",
|
||||||
"core.mod_imscp": "IMS content package",
|
"core.mod_imscp": "IMS content package",
|
||||||
"core.mod_label": "Label",
|
"core.mod_label": "Label",
|
||||||
|
|
|
@ -1820,7 +1820,7 @@ export class CoreSite {
|
||||||
|
|
||||||
this.lastAutoLogin = this.timeUtils.timestamp();
|
this.lastAutoLogin = this.timeUtils.timestamp();
|
||||||
|
|
||||||
return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + url;
|
return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + encodeURIComponent(url);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|
||||||
// Couldn't get autologin key, return the same URL.
|
// Couldn't get autologin key, return the same URL.
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { CORE_PUSHNOTIFICATIONS_PROVIDERS } from '@core/pushnotifications/pushno
|
||||||
import { IONIC_NATIVE_PROVIDERS } from '@core/emulator/emulator.module';
|
import { IONIC_NATIVE_PROVIDERS } from '@core/emulator/emulator.module';
|
||||||
import { CORE_EDITOR_PROVIDERS } from '@core/editor/editor.module';
|
import { CORE_EDITOR_PROVIDERS } from '@core/editor/editor.module';
|
||||||
import { CORE_SEARCH_PROVIDERS } from '@core/search/search.module';
|
import { CORE_SEARCH_PROVIDERS } from '@core/search/search.module';
|
||||||
|
import { CORE_XAPI_PROVIDERS } from '@core/xapi/xapi.module';
|
||||||
|
|
||||||
// Import only this provider to prevent circular dependencies.
|
// Import only this provider to prevent circular dependencies.
|
||||||
import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins';
|
import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins';
|
||||||
|
@ -243,7 +244,7 @@ export class CoreCompileProvider {
|
||||||
.concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS)
|
.concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS)
|
||||||
.concat(CORE_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS)
|
.concat(CORE_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS)
|
||||||
.concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS).concat(CORE_EDITOR_PROVIDERS)
|
.concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS).concat(CORE_EDITOR_PROVIDERS)
|
||||||
.concat(CORE_SEARCH_PROVIDERS).concat(ADDON_MOD_H5P_ACTIVITY_PROVIDERS);
|
.concat(CORE_SEARCH_PROVIDERS).concat(ADDON_MOD_H5P_ACTIVITY_PROVIDERS).concat(CORE_XAPI_PROVIDERS);
|
||||||
|
|
||||||
// We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
|
// We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
|
||||||
for (const i in providers) {
|
for (const i in providers) {
|
||||||
|
|
|
@ -280,8 +280,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
*
|
*
|
||||||
* @param event Event.
|
* @param event Event.
|
||||||
*/
|
*/
|
||||||
gotoBlog(event: any): void {
|
gotoBlog(event: any): Promise<any> {
|
||||||
this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id });
|
return this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -244,6 +244,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
|
||||||
|
|
||||||
protected ROOT_CACHE_KEY = 'mmCourse:';
|
protected ROOT_CACHE_KEY = 'mmCourse:';
|
||||||
protected statusCache = new CoreCache();
|
protected statusCache = new CoreCache();
|
||||||
|
protected featurePrefix = 'CoreCourseModuleDelegate_';
|
||||||
protected handlerNameProperty = 'modName';
|
protected handlerNameProperty = 'modName';
|
||||||
|
|
||||||
// Promises for check updates, to prevent performing the same request twice at the same time.
|
// Promises for check updates, to prevent performing the same request twice at the same time.
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
// (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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle display options included in the URL and put them in the H5PIntegration object if it exists.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (window.H5PIntegration && window.H5PIntegration.contents && location.search) {
|
|
||||||
var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]];
|
|
||||||
|
|
||||||
if (contentData) {
|
|
||||||
contentData.displayOptions = contentData.displayOptions || {};
|
|
||||||
|
|
||||||
var search = location.search.replace(/^\?/, ''),
|
|
||||||
split = search.split('&');
|
|
||||||
|
|
||||||
split.forEach(function(param) {
|
|
||||||
var nameAndValue = param.split('=');
|
|
||||||
if (nameAndValue.length == 2) {
|
|
||||||
contentData.displayOptions[nameAndValue[0]] = nameAndValue[1] === '1' || nameAndValue[1] === 'true';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -71,6 +71,21 @@ H5PEmbedCommunicator = (function() {
|
||||||
// Parent origin can be anything.
|
// Parent origin can be anything.
|
||||||
window.parent.postMessage(data, '*');
|
window.parent.postMessage(data, '*');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a xAPI statement to LMS.
|
||||||
|
*
|
||||||
|
* @param {string} component
|
||||||
|
* @param {Object} statements
|
||||||
|
*/
|
||||||
|
self.post = function(component, statements) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
context: 'moodleapp',
|
||||||
|
action: 'xapi_post_statement',
|
||||||
|
component: component,
|
||||||
|
statements: statements,
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
|
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
|
||||||
|
@ -150,6 +165,38 @@ document.onreadystatechange = function() {
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get emitted xAPI data.
|
||||||
|
H5P.externalDispatcher.on('xAPI', function(event) {
|
||||||
|
var moodlecomponent = H5P.getMoodleComponent();
|
||||||
|
if (moodlecomponent == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip malformed events.
|
||||||
|
var hasStatement = event && event.data && event.data.statement;
|
||||||
|
if (!hasStatement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statement = event.data.statement;
|
||||||
|
var validVerb = statement.verb && statement.verb.id;
|
||||||
|
if (!validVerb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered'
|
||||||
|
|| statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed';
|
||||||
|
|
||||||
|
var isChild = statement.context && statement.context.contextActivities &&
|
||||||
|
statement.context.contextActivities.parent &&
|
||||||
|
statement.context.contextActivities.parent[0] &&
|
||||||
|
statement.context.contextActivities.parent[0].id;
|
||||||
|
|
||||||
|
if (isCompleted && !isChild) {
|
||||||
|
var statements = H5P.getXAPIStatements(this.contentId, statement);
|
||||||
|
H5PEmbedCommunicator.post(moodlecomponent, statements);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Trigger initial resize for instance.
|
// Trigger initial resize for instance.
|
||||||
H5P.trigger(instance, 'resize');
|
H5P.trigger(instance, 'resize');
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
H5P.findInstanceFromId = function (contentId) {
|
||||||
|
if (!contentId) {
|
||||||
|
return H5P.instances[0];
|
||||||
|
}
|
||||||
|
if (H5P.instances !== undefined) {
|
||||||
|
for (var i = 0; i < H5P.instances.length; i++) {
|
||||||
|
if (H5P.instances[i].contentId === contentId) {
|
||||||
|
return H5P.instances[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
H5P.getXAPIStatements = function (contentId, statement) {
|
||||||
|
var statements = [];
|
||||||
|
var instance = H5P.findInstanceFromId(contentId);
|
||||||
|
if (!instance){
|
||||||
|
return statements;
|
||||||
|
}
|
||||||
|
if (instance.getXAPIData == undefined) {
|
||||||
|
var xAPIData = {
|
||||||
|
statement: statement
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
var xAPIData = instance.getXAPIData();
|
||||||
|
}
|
||||||
|
if (xAPIData.statement != undefined) {
|
||||||
|
statements.push(xAPIData.statement);
|
||||||
|
}
|
||||||
|
if (xAPIData.children != undefined) {
|
||||||
|
statements = statements.concat(xAPIData.children.map(a => a.statement));
|
||||||
|
}
|
||||||
|
return statements;
|
||||||
|
};
|
||||||
|
H5P.getMoodleComponent = function () {
|
||||||
|
if (H5PIntegration.moodleComponent) {
|
||||||
|
return H5PIntegration.moodleComponent;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
|
@ -0,0 +1,46 @@
|
||||||
|
// (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.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle params included in the URL and put them in the H5PIntegration object if it exists.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (window.H5PIntegration && window.H5PIntegration.contents && location.search) {
|
||||||
|
var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]];
|
||||||
|
|
||||||
|
var search = location.search.replace(/^\?/, '');
|
||||||
|
var split = search.split('&');
|
||||||
|
|
||||||
|
split.forEach(function(param) {
|
||||||
|
var nameAndValue = param.split('=');
|
||||||
|
|
||||||
|
if (nameAndValue[0] == 'displayOptions' && contentData) {
|
||||||
|
try {
|
||||||
|
contentData.displayOptions = contentData.displayOptions || {};
|
||||||
|
|
||||||
|
var displayOptions = JSON.parse(decodeURIComponent(nameAndValue[1]));
|
||||||
|
|
||||||
|
if (displayOptions && typeof displayOptions == 'object') {
|
||||||
|
Object.assign(contentData.displayOptions, displayOptions);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing display options', decodeURIComponent(nameAndValue[1]));
|
||||||
|
}
|
||||||
|
} else if (nameAndValue[0] == 'component') {
|
||||||
|
window.H5PIntegration.moodleComponent = nameAndValue[1];
|
||||||
|
} else if (nameAndValue[0] == 'trackingUrl' && contentData) {
|
||||||
|
contentData.url = nameAndValue[1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -615,6 +615,8 @@ export class CoreH5PCore {
|
||||||
urls.push(libUrl + script);
|
urls.push(libUrl + script);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
urls.push(CoreTextUtils.instance.concatenatePaths(libUrl, 'moodle/js/h5p_overrides.js'));
|
||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites';
|
||||||
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
|
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
|
||||||
import { CoreTextUtils } from '@providers/utils/text';
|
import { CoreTextUtils } from '@providers/utils/text';
|
||||||
import { CoreUtils } from '@providers/utils/utils';
|
import { CoreUtils } from '@providers/utils/utils';
|
||||||
|
import { CoreUser } from '@core/user/providers/user';
|
||||||
import { CoreH5P } from '../providers/h5p';
|
import { CoreH5P } from '../providers/h5p';
|
||||||
import { CoreH5PCore, CoreH5PDisplayOptions } from './core';
|
import { CoreH5PCore, CoreH5PDisplayOptions } from './core';
|
||||||
import { FileEntry } from '@ionic-native/file';
|
import { FileEntry } from '@ionic-native/file';
|
||||||
|
@ -90,6 +91,13 @@ export class CoreH5PHelper {
|
||||||
static async getCoreSettings(siteId?: string): Promise<any> {
|
static async getCoreSettings(siteId?: string): Promise<any> {
|
||||||
|
|
||||||
const site = await CoreSites.instance.getSite(siteId);
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
let user;
|
||||||
|
|
||||||
|
try {
|
||||||
|
user = await CoreUser.instance.getProfile(site.getUserId(), undefined, true);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
|
||||||
const basePath = CoreFile.instance.getBasePathInstant();
|
const basePath = CoreFile.instance.getBasePathInstant();
|
||||||
const ajaxPaths = {
|
const ajaxPaths = {
|
||||||
|
@ -110,7 +118,7 @@ export class CoreH5PHelper {
|
||||||
l10n: {
|
l10n: {
|
||||||
H5P: CoreH5P.instance.h5pCore.getLocalization(),
|
H5P: CoreH5P.instance.h5pCore.getLocalization(),
|
||||||
},
|
},
|
||||||
user: [],
|
user: {name: site.getInfo().fullname, mail: user && user.email},
|
||||||
hubIsEnabled: false,
|
hubIsEnabled: false,
|
||||||
reportingIsEnabled: false,
|
reportingIsEnabled: false,
|
||||||
crossorigin: null,
|
crossorigin: null,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites';
|
||||||
import { CoreTextUtils } from '@providers/utils/text';
|
import { CoreTextUtils } from '@providers/utils/text';
|
||||||
import { CoreUrlUtils } from '@providers/utils/url';
|
import { CoreUrlUtils } from '@providers/utils/url';
|
||||||
import { CoreUtils } from '@providers/utils/utils';
|
import { CoreUtils } from '@providers/utils/utils';
|
||||||
|
import { CoreXAPI } from '@core/xapi/providers/xapi';
|
||||||
import { CoreH5P } from '../providers/h5p';
|
import { CoreH5P } from '../providers/h5p';
|
||||||
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core';
|
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core';
|
||||||
import { CoreH5PHelper } from './helper';
|
import { CoreH5PHelper } from './helper';
|
||||||
|
@ -81,7 +82,7 @@ export class CoreH5PPlayer {
|
||||||
resizeCode: this.getResizeCode(),
|
resizeCode: this.getResizeCode(),
|
||||||
title: content.slug,
|
title: content.slug,
|
||||||
displayOptions: {},
|
displayOptions: {},
|
||||||
url: this.getEmbedUrl(site.getURL(), h5pUrl),
|
url: '', // It will be filled using dynamic params if needed.
|
||||||
contentUrl: contentUrl,
|
contentUrl: contentUrl,
|
||||||
metadata: content.metadata,
|
metadata: content.metadata,
|
||||||
contentUserData: [
|
contentUserData: [
|
||||||
|
@ -109,9 +110,9 @@ export class CoreH5PPlayer {
|
||||||
html += '<script type="text/javascript">var H5PIntegration = ' +
|
html += '<script type="text/javascript">var H5PIntegration = ' +
|
||||||
JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>';
|
JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>';
|
||||||
|
|
||||||
// Add our own script to handle the display options.
|
// Add our own script to handle the params.
|
||||||
html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths(
|
html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths(
|
||||||
this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/displayoptions.js') + '"></script>';
|
this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/params.js') + '"></script>';
|
||||||
|
|
||||||
html += '</head><body>';
|
html += '</head><body>';
|
||||||
|
|
||||||
|
@ -241,20 +242,34 @@ export class CoreH5PPlayer {
|
||||||
*
|
*
|
||||||
* @param fileUrl URL of the H5P package.
|
* @param fileUrl URL of the H5P package.
|
||||||
* @param displayOptions Display options.
|
* @param displayOptions Display options.
|
||||||
|
* @param component Component to send xAPI events to.
|
||||||
|
* @param contextId Context ID where the H5P is. Required for tracking.
|
||||||
* @param siteId The site ID. If not defined, current site.
|
* @param siteId The site ID. If not defined, current site.
|
||||||
* @return Promise resolved with the file URL if exists, rejected otherwise.
|
* @return Promise resolved with the file URL if exists, rejected otherwise.
|
||||||
*/
|
*/
|
||||||
async getContentIndexFileUrl(fileUrl: string, displayOptions?: CoreH5PDisplayOptions, siteId?: string): Promise<string> {
|
async getContentIndexFileUrl(fileUrl: string, displayOptions?: CoreH5PDisplayOptions, component?: string, contextId?: number,
|
||||||
|
siteId?: string): Promise<string> {
|
||||||
|
|
||||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId);
|
const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId);
|
||||||
|
|
||||||
// Add display options to the URL.
|
// Add display options and component to the URL.
|
||||||
const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId);
|
const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId);
|
||||||
|
|
||||||
displayOptions = this.h5pCore.fixDisplayOptions(displayOptions, data.id);
|
displayOptions = this.h5pCore.fixDisplayOptions(displayOptions, data.id);
|
||||||
|
|
||||||
return CoreUrlUtils.instance.addParamsToUrl(path, displayOptions, undefined, true);
|
const params = {
|
||||||
|
displayOptions: JSON.stringify(displayOptions),
|
||||||
|
component: component || '',
|
||||||
|
trackingUrl: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (contextId) {
|
||||||
|
params.trackingUrl = await CoreXAPI.instance.getUrl(contextId, 'activity', siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreUrlUtils.instance.addParamsToUrl(path, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,6 +38,8 @@ export class CoreH5PIframeComponent implements OnChanges {
|
||||||
@Input() fileUrl?: string; // The URL of the H5P file. If not supplied, onlinePlayerUrl is required.
|
@Input() fileUrl?: string; // The URL of the H5P file. If not supplied, onlinePlayerUrl is required.
|
||||||
@Input() displayOptions?: CoreH5PDisplayOptions; // Display options.
|
@Input() displayOptions?: CoreH5PDisplayOptions; // Display options.
|
||||||
@Input() onlinePlayerUrl?: string; // The URL of the online player to display the H5P package.
|
@Input() onlinePlayerUrl?: string; // The URL of the online player to display the H5P package.
|
||||||
|
@Input() trackComponent?: string; // Component to send xAPI events to.
|
||||||
|
@Input() contextId?: number; // Context ID. Required for tracking.
|
||||||
@Output() onIframeUrlSet = new EventEmitter<{src: string, online: boolean}>();
|
@Output() onIframeUrlSet = new EventEmitter<{src: string, online: boolean}>();
|
||||||
@Output() onIframeLoaded = new EventEmitter<void>();
|
@Output() onIframeLoaded = new EventEmitter<void>();
|
||||||
|
|
||||||
|
@ -93,7 +95,7 @@ export class CoreH5PIframeComponent implements OnChanges {
|
||||||
this.iframeSrc = localUrl;
|
this.iframeSrc = localUrl;
|
||||||
} else {
|
} else {
|
||||||
this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
|
this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
|
||||||
this.site.getURL(), this.fileUrl, this.displayOptions);
|
this.site.getURL(), this.fileUrl, this.displayOptions, this.trackComponent);
|
||||||
|
|
||||||
// Never allow downloading in the app. This will only work if the user is allowed to change the params.
|
// Never allow downloading in the app. This will only work if the user is allowed to change the params.
|
||||||
const src = this.onlinePlayerUrl.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1',
|
const src = this.onlinePlayerUrl.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1',
|
||||||
|
@ -121,7 +123,8 @@ export class CoreH5PIframeComponent implements OnChanges {
|
||||||
*/
|
*/
|
||||||
protected async getLocalUrl(): Promise<string> {
|
protected async getLocalUrl(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions, this.siteId);
|
const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions,
|
||||||
|
this.trackComponent, this.contextId, this.siteId);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -135,7 +138,7 @@ export class CoreH5PIframeComponent implements OnChanges {
|
||||||
|
|
||||||
// File treated. Try to get the index file URL again.
|
// File treated. Try to get the index file URL again.
|
||||||
const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions,
|
const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions,
|
||||||
this.siteId);
|
this.trackComponent, this.contextId, this.siteId);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -391,7 +391,7 @@ export class CoreH5PProvider {
|
||||||
* @param siteId Site ID (empty for current site).
|
* @param siteId Site ID (empty for current site).
|
||||||
* @return Promise resolved when the data is invalidated.
|
* @return Promise resolved when the data is invalidated.
|
||||||
*/
|
*/
|
||||||
async invalidateAvailableInContexts(url: string, siteId?: string): Promise<void> {
|
async invalidateGetTrustedH5PFile(url: string, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.instance.getSite(siteId);
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url));
|
await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url));
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreSites, CoreSiteSchema } from '@providers/sites';
|
||||||
|
|
||||||
|
import { makeSingleton } from '@singletons/core.singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle offline xAPI.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CoreXAPIOfflineProvider {
|
||||||
|
|
||||||
|
// Variables for database.
|
||||||
|
static STATEMENTS_TABLE = 'core_xapi_statements';
|
||||||
|
|
||||||
|
protected siteSchema: CoreSiteSchema = {
|
||||||
|
name: 'CoreXAPIOfflineProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: CoreXAPIOfflineProvider.STATEMENTS_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'INTEGER',
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contextid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component',
|
||||||
|
type: 'TEXT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'statements',
|
||||||
|
type: 'TEXT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timecreated',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'courseid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'extra',
|
||||||
|
type: 'TEXT'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
CoreSites.instance.registerSiteSchema(this.siteSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are offline statements to send for a context.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: true if has offline statements, false otherwise.
|
||||||
|
*/
|
||||||
|
async contextHasStatements(contextId: number, siteId?: string): Promise<boolean> {
|
||||||
|
const statementsList = await this.getContextStatements(contextId, siteId);
|
||||||
|
|
||||||
|
return statementsList && statementsList.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete certain statements.
|
||||||
|
*
|
||||||
|
* @param id ID of the statements.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async deleteStatements(id: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {id});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all statements of a certain context.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async deleteStatementsForContext(contextId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {contextid: contextId});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all offline statements.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with all the data.
|
||||||
|
*/
|
||||||
|
async getAllStatements(siteId?: string): Promise<CoreXAPIOfflineStatementsDBData[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return site.getDb().getRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, undefined, 'timecreated ASC');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statements for a context.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the data.
|
||||||
|
*/
|
||||||
|
async getContextStatements(contextId: number, siteId?: string): Promise<CoreXAPIOfflineStatementsDBData[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return site.getDb().getRecords(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {contextid: contextId}, 'timecreated ASC');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get certain statements.
|
||||||
|
*
|
||||||
|
* @param id ID of the statements.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the data.
|
||||||
|
*/
|
||||||
|
async getStatements(id: number, siteId?: string): Promise<CoreXAPIOfflineStatementsDBData> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return site.getDb().getRecord(CoreXAPIOfflineProvider.STATEMENTS_TABLE, {id});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save statements.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param component Component to send the statements to.
|
||||||
|
* @param statements Statements (JSON-encoded).
|
||||||
|
* @param options Options.
|
||||||
|
* @return Promise resolved when statements are successfully saved.
|
||||||
|
*/
|
||||||
|
async saveStatements(contextId: number, component: string, statements: string, options?: CoreXAPIOfflineSaveStatementsOptions)
|
||||||
|
: Promise<void> {
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(options.siteId);
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
contextid: contextId,
|
||||||
|
component: component,
|
||||||
|
statements: statements,
|
||||||
|
timecreated: Date.now(),
|
||||||
|
courseid: options.courseId,
|
||||||
|
extra: options.extra,
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(CoreXAPIOfflineProvider.STATEMENTS_TABLE, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreXAPIOffline extends makeSingleton(CoreXAPIOfflineProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB data stored for statements.
|
||||||
|
*/
|
||||||
|
export type CoreXAPIOfflineStatementsDBData = {
|
||||||
|
id: number; // ID.
|
||||||
|
contextid: number; // Context ID of the statements.
|
||||||
|
component: string; // Component to send the statements to.
|
||||||
|
statements: string; // Statements (JSON-encoded).
|
||||||
|
timecreated: number; // When were the statements created.
|
||||||
|
courseid?: number; // Course ID if the context is inside a course.
|
||||||
|
extra?: string; // Extra data.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to pass to saveStatements function.
|
||||||
|
*/
|
||||||
|
export type CoreXAPIOfflineSaveStatementsOptions = {
|
||||||
|
courseId?: number; // Course ID if the context is inside a course.
|
||||||
|
extra?: string; // Extra data to store.
|
||||||
|
siteId?: string; // Site ID. If not defined, current site.
|
||||||
|
};
|
|
@ -0,0 +1,143 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreApp } from '@providers/app';
|
||||||
|
import { CoreSites } from '@providers/sites';
|
||||||
|
import { CoreTextUtils } from '@providers/utils/text';
|
||||||
|
import { CoreUtils } from '@providers/utils/utils';
|
||||||
|
import { CoreSite } from '@classes/site';
|
||||||
|
import { CoreXAPIOffline, CoreXAPIOfflineSaveStatementsOptions } from './offline';
|
||||||
|
|
||||||
|
import { makeSingleton } from '@singletons/core.singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to provide XAPI functionalities.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CoreXAPIProvider {
|
||||||
|
|
||||||
|
protected ROOT_CACHE_KEY = 'CoreXAPI:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not WS to post XAPI statement is available.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with true if ws is available, false otherwise.
|
||||||
|
* @since 3.9
|
||||||
|
*/
|
||||||
|
async canPostStatements(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return this.canPostStatementsInSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not WS to post XAPI statement is available in a certain site.
|
||||||
|
*
|
||||||
|
* @param site Site. If not defined, current site.
|
||||||
|
* @return Promise resolved with true if ws is available, false otherwise.
|
||||||
|
* @since 3.9
|
||||||
|
*/
|
||||||
|
canPostStatementsInSite(site?: CoreSite): boolean {
|
||||||
|
site = site || CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
return site.wsAvailable('core_xapi_statement_post');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get URL for XAPI events.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param type Type (e.g. 'activity').
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async getUrl(contextId: number, type: string, siteId?: string): Promise<string> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return CoreTextUtils.instance.concatenatePaths(site.getURL(), `xapi/${type}/${contextId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post statements.
|
||||||
|
*
|
||||||
|
* @param contextId Context ID.
|
||||||
|
* @param component Component.
|
||||||
|
* @param json JSON string to send.
|
||||||
|
* @param options Options.
|
||||||
|
* @return Promise resolved with boolean: true if response was sent to server, false if stored in device.
|
||||||
|
*/
|
||||||
|
async postStatements(contextId: number, component: string, json: string, options?: CoreXAPIPostStatementsOptions)
|
||||||
|
: Promise<boolean> {
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
// Convenience function to store a message to be synchronized later.
|
||||||
|
const storeOffline = async (): Promise<boolean> => {
|
||||||
|
await CoreXAPIOffline.instance.saveStatements(contextId, component, json, options);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!CoreApp.instance.isOnline() || options.offline) {
|
||||||
|
// App is offline, store the action.
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.postStatementsOnline(component, json, options.siteId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.instance.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
// Couldn't connect to server, store it offline.
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post statements. It will fail if offline or cannot connect.
|
||||||
|
*
|
||||||
|
* @param component Component.
|
||||||
|
* @param json JSON string to send.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async postStatementsOnline(component: string, json: string, siteId?: string): Promise<number[]> {
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
component: component,
|
||||||
|
requestjson: json,
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.write('core_xapi_statement_post', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreXAPI extends makeSingleton(CoreXAPIProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to pass to postStatements function.
|
||||||
|
*/
|
||||||
|
export type CoreXAPIPostStatementsOptions = CoreXAPIOfflineSaveStatementsOptions & {
|
||||||
|
offline?: boolean; // Whether to force storing it in offline.
|
||||||
|
};
|
|
@ -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 { CoreXAPIProvider } from './providers/xapi';
|
||||||
|
import { CoreXAPIOfflineProvider } from './providers/offline';
|
||||||
|
|
||||||
|
// List of providers (without handlers).
|
||||||
|
export const CORE_XAPI_PROVIDERS: any[] = [
|
||||||
|
CoreXAPIProvider,
|
||||||
|
CoreXAPIOfflineProvider,
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
CoreXAPIProvider,
|
||||||
|
CoreXAPIOfflineProvider,
|
||||||
|
],
|
||||||
|
exports: []
|
||||||
|
})
|
||||||
|
export class CoreXAPIModule { }
|
|
@ -43,6 +43,7 @@
|
||||||
"confirmdeletefile": "Are you sure you want to delete this file?",
|
"confirmdeletefile": "Are you sure you want to delete this file?",
|
||||||
"confirmgotabroot": "Are you sure you want to go back to {{name}}?",
|
"confirmgotabroot": "Are you sure you want to go back to {{name}}?",
|
||||||
"confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
|
"confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
|
||||||
|
"confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have some unsaved changes they will be lost.",
|
||||||
"confirmloss": "Are you sure? All changes will be lost.",
|
"confirmloss": "Are you sure? All changes will be lost.",
|
||||||
"confirmopeninbrowser": "Do you want to open it in a web browser?",
|
"confirmopeninbrowser": "Do you want to open it in a web browser?",
|
||||||
"considereddigitalminor": "You are too young to create an account on this site.",
|
"considereddigitalminor": "You are too young to create an account on this site.",
|
||||||
|
@ -153,6 +154,7 @@
|
||||||
"mod_folder": "Folder",
|
"mod_folder": "Folder",
|
||||||
"mod_forum": "Forum",
|
"mod_forum": "Forum",
|
||||||
"mod_glossary": "Glossary",
|
"mod_glossary": "Glossary",
|
||||||
|
"mod_h5pactivity": "H5P",
|
||||||
"mod_ims": "IMS content package",
|
"mod_ims": "IMS content package",
|
||||||
"mod_imscp": "IMS content package",
|
"mod_imscp": "IMS content package",
|
||||||
"mod_label": "Label",
|
"mod_label": "Label",
|
||||||
|
|
|
@ -66,6 +66,7 @@ export class CoreTextUtilsProvider {
|
||||||
{old: /_mmaModFolder/g, new: '_AddonModFolder'},
|
{old: /_mmaModFolder/g, new: '_AddonModFolder'},
|
||||||
{old: /_mmaModForum/g, new: '_AddonModForum'},
|
{old: /_mmaModForum/g, new: '_AddonModForum'},
|
||||||
{old: /_mmaModGlossary/g, new: '_AddonModGlossary'},
|
{old: /_mmaModGlossary/g, new: '_AddonModGlossary'},
|
||||||
|
{old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity'},
|
||||||
{old: /_mmaModImscp/g, new: '_AddonModImscp'},
|
{old: /_mmaModImscp/g, new: '_AddonModImscp'},
|
||||||
{old: /_mmaModLabel/g, new: '_AddonModLabel'},
|
{old: /_mmaModLabel/g, new: '_AddonModLabel'},
|
||||||
{old: /_mmaModLesson/g, new: '_AddonModLesson'},
|
{old: /_mmaModLesson/g, new: '_AddonModLesson'},
|
||||||
|
|
Loading…
Reference in New Issue