Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/langindex.json
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@
"addon.mod_h5pactivity.no_compatible_track": "h5pactivity",
"addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp",
"addon.mod_h5pactivity.outcome": "h5pactivity",
"addon.mod_h5pactivity.previewmode": "h5pactivity",
"addon.mod_h5pactivity.result_fill-in": "h5pactivity",
"addon.mod_h5pactivity.result_other": "h5pactivity",
"addon.mod_h5pactivity.review_my_attempts": "h5pactivity",
Expand Down Expand Up @@ -1407,6 +1408,7 @@
"core.confirmdeletefile": "repository",
"core.confirmgotabroot": "local_moodlemobileapp",
"core.confirmgotabrootdefault": "local_moodlemobileapp",
"core.confirmleaveunknownchanges": "local_moodlemobileapp",
"core.confirmloss": "local_moodlemobileapp",
"core.confirmopeninbrowser": "local_moodlemobileapp",
"core.considereddigitalminor": "moodle",
Expand Down Expand Up @@ -1860,6 +1862,7 @@
"core.mod_folder": "folder/pluginname",
"core.mod_forum": "forum/pluginname",
"core.mod_glossary": "glossary/pluginname",
"core.mod_h5pactivity": "h5pactivity/pluginname",
"core.mod_ims": "imscp/pluginname",
"core.mod_imscp": "imscp/pluginname",
"core.mod_label": "label/pluginname",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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="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="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="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>
Expand All @@ -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>

<!-- 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. -->
<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-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-item text-wrap *ngIf="stateMessage">
<p >{{ stateMessage | translate }}</p>
Expand All @@ -39,5 +50,5 @@ <h2 *ngIf="progressMessage">{{ progressMessage | translate }}</h2>
</ion-item>
</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>
151 changes: 143 additions & 8 deletions src/addon/mod/h5pactivity/components/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main
import { CoreH5P } from '@core/h5p/providers/h5p';
import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
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 { CoreSite } from '@classes/site';

import {
AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo
} from '../../providers/h5pactivity';
import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync';

/**
* Component that displays an H5P activity entry page.
Expand Down Expand Up @@ -57,17 +60,26 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
fileUrl: string; // The fileUrl to use to play the package.
state: string; // State of the file.
siteCanDownload: boolean;
trackComponent: string; // Component for tracking.
hasOffline: boolean;
isOpeningPage: boolean;

protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
protected site: CoreSite;
protected observer;
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;

constructor(injector: Injector,
@Optional() protected content: Content) {
super(injector, content);

this.site = this.sitesProvider.getCurrentSite();
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);
}

/**
Expand Down Expand Up @@ -96,23 +108,30 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
*/
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
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.description = this.h5pActivity.intro;
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);

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);
if (sync) {
await this.syncActivity(showErrors);
}

await Promise.all([
this.checkHasOffline(),
this.fetchAccessInfo(),
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) {
// Cannot download the file or already downloaded, play the package directly.
this.play();
Expand All @@ -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.
*
* @return Promise resolved when done.
*/
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);
}

/**
Expand Down Expand Up @@ -322,14 +350,121 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
/**
* Go to view user events.
*/
viewMyAttempts(): void {
this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id});
async viewMyAttempts(): Promise<void> {
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;
}
}

/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.observer && this.observer.off();
window.removeEventListener('message', this.messageListenerFunction);
}
}
11 changes: 10 additions & 1 deletion src/addon/mod/h5pactivity/h5pactivity.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@

import { NgModule } from '@angular/core';

import { CoreCronDelegate } from '@providers/cron';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';

import { AddonModH5PActivityComponentsModule } from './components/components.module';
import { AddonModH5PActivityModuleHandler } from './providers/module-handler';
import { AddonModH5PActivityProvider } from './providers/h5pactivity';
import { AddonModH5PActivitySyncProvider } from './providers/sync';
import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler';
import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler';
import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler';
import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler';

// List of providers (without handlers).
export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
AddonModH5PActivityProvider,
AddonModH5PActivitySyncProvider,
];

@NgModule({
Expand All @@ -38,10 +42,12 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
],
providers: [
AddonModH5PActivityProvider,
AddonModH5PActivitySyncProvider,
AddonModH5PActivityModuleHandler,
AddonModH5PActivityPrefetchHandler,
AddonModH5PActivityIndexLinkHandler,
AddonModH5PActivityReportLinkHandler,
AddonModH5PActivitySyncCronHandler,
]
})
export class AddonModH5PActivityModule {
Expand All @@ -51,11 +57,14 @@ export class AddonModH5PActivityModule {
prefetchHandler: AddonModH5PActivityPrefetchHandler,
linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModH5PActivityIndexLinkHandler,
reportLinkHandler: AddonModH5PActivityReportLinkHandler) {
reportLinkHandler: AddonModH5PActivityReportLinkHandler,
cronDelegate: CoreCronDelegate,
syncHandler: AddonModH5PActivitySyncCronHandler) {

moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler);
linksDelegate.registerHandler(indexHandler);
linksDelegate.registerHandler(reportLinkHandler);
cronDelegate.register(syncHandler);
}
}
1 change: 1 addition & 0 deletions src/addon/mod/h5pactivity/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
"offlinedisabledwarning": "You will need to be online to view the H5P package.",
"outcome": "Outcome",
"previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
"result_fill-in": "Fill-in text",
"result_other": "Unkown interaction type",
"review_my_attempts": "View my attempts",
Expand Down
16 changes: 16 additions & 0 deletions src/addon/mod/h5pactivity/pages/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@

import { Component, ViewChild } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtils } from '@providers/utils/dom';
import { AddonModH5PActivityIndexComponent } from '../../components/index/index';
import { AddonModH5PActivityData } from '../../providers/h5pactivity';

import { Translate } from '@singletons/core.singletons';

/**
* Page that displays an H5P activity.
*/
Expand Down Expand Up @@ -46,4 +49,17 @@ export class AddonModH5PActivityIndexPage {
updateData(h5p: AddonModH5PActivityData): void {
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'));
}
}
Loading