MOBILE-4670 h5p: Use online player if package has missing dependencies
parent
dc7bea0b2e
commit
4cbb3fe7f0
|
@ -1571,6 +1571,7 @@
|
||||||
"core.confirmopeninbrowser": "local_moodlemobileapp",
|
"core.confirmopeninbrowser": "local_moodlemobileapp",
|
||||||
"core.confirmremoveselectedfile": "local_moodlemobileapp",
|
"core.confirmremoveselectedfile": "local_moodlemobileapp",
|
||||||
"core.confirmremoveselectedfiles": "local_moodlemobileapp",
|
"core.confirmremoveselectedfiles": "local_moodlemobileapp",
|
||||||
|
"core.connectandtryagain": "local_moodlemobileapp",
|
||||||
"core.connectionlost": "local_moodlemobileapp",
|
"core.connectionlost": "local_moodlemobileapp",
|
||||||
"core.considereddigitalminor": "moodle",
|
"core.considereddigitalminor": "moodle",
|
||||||
"core.contactsupport": "local_moodlemobileapp",
|
"core.contactsupport": "local_moodlemobileapp",
|
||||||
|
@ -1588,11 +1589,14 @@
|
||||||
"core.copytoclipboard": "local_moodlemobileapp",
|
"core.copytoclipboard": "local_moodlemobileapp",
|
||||||
"core.course": "moodle",
|
"core.course": "moodle",
|
||||||
"core.course.activitydisabled": "local_moodlemobileapp",
|
"core.course.activitydisabled": "local_moodlemobileapp",
|
||||||
|
"core.course.activitynotavailableoffline": "local_moodlemobileapp",
|
||||||
"core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp",
|
"core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp",
|
||||||
|
"core.course.activityrequiresconnection": "local_moodlemobileapp",
|
||||||
"core.course.allsections": "local_moodlemobileapp",
|
"core.course.allsections": "local_moodlemobileapp",
|
||||||
"core.course.aria:sectionprogress": "local_moodlemobileapp",
|
"core.course.aria:sectionprogress": "local_moodlemobileapp",
|
||||||
"core.course.availablespace": "local_moodlemobileapp",
|
"core.course.availablespace": "local_moodlemobileapp",
|
||||||
"core.course.cannotdeletewhiledownloading": "local_moodlemobileapp",
|
"core.course.cannotdeletewhiledownloading": "local_moodlemobileapp",
|
||||||
|
"core.course.changesofflinemaybelost": "local_moodlemobileapp",
|
||||||
"core.course.communicationroomlink": "course",
|
"core.course.communicationroomlink": "course",
|
||||||
"core.course.completion_automatic:done": "course",
|
"core.course.completion_automatic:done": "course",
|
||||||
"core.course.completion_automatic:failed": "course",
|
"core.course.completion_automatic:failed": "course",
|
||||||
|
@ -2269,6 +2273,7 @@
|
||||||
"core.mygroups": "group",
|
"core.mygroups": "group",
|
||||||
"core.name": "moodle",
|
"core.name": "moodle",
|
||||||
"core.needhelp": "local_moodlemobileapp",
|
"core.needhelp": "local_moodlemobileapp",
|
||||||
|
"core.needinternettoaccessit": "local_moodlemobileapp",
|
||||||
"core.networkerroriframemsg": "local_moodlemobileapp",
|
"core.networkerroriframemsg": "local_moodlemobileapp",
|
||||||
"core.networkerrormsg": "local_moodlemobileapp",
|
"core.networkerrormsg": "local_moodlemobileapp",
|
||||||
"core.never": "moodle",
|
"core.never": "moodle",
|
||||||
|
|
|
@ -27,15 +27,21 @@
|
||||||
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
|
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
|
||||||
[courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()" />
|
[courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()" />
|
||||||
|
|
||||||
<!-- Offline disabled. -->
|
<!-- User tried to play in offline a package that must be played in online. -->
|
||||||
<ion-card class="core-warning-card" *ngIf="!siteCanDownload && playing">
|
@if (triedToPlay && !isOnline && (!siteCanDownload || hasMissingDependencies)) {
|
||||||
|
<ion-card class="core-warning-card">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
|
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@if (!siteCanDownload) {
|
||||||
{{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
|
{{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
|
||||||
|
} @else {
|
||||||
|
{{ 'core.course.activitynotavailableoffline' | translate }} {{ 'core.needinternettoaccessit' | translate }}
|
||||||
|
}
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Preview mode. -->
|
<!-- Preview mode. -->
|
||||||
<ion-card class="core-warning-card" *ngIf="accessInfo && !trackComponent">
|
<ion-card class="core-warning-card" *ngIf="accessInfo && !trackComponent">
|
||||||
|
@ -69,7 +75,7 @@
|
||||||
|
|
||||||
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"
|
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"
|
||||||
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context" [enableInAppFullscreen]="true" [saveFreq]="saveFreq"
|
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context" [enableInAppFullscreen]="true" [saveFreq]="saveFreq"
|
||||||
[state]="contentState" />
|
[state]="contentState" [component]="component" [componentId]="componentId" [fileTimemodified]="deployedFile?.timemodified" />
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id" />
|
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id" />
|
||||||
|
|
|
@ -53,6 +53,11 @@ import {
|
||||||
ADDON_MOD_H5PACTIVITY_STATE_ID,
|
ADDON_MOD_H5PACTIVITY_STATE_ID,
|
||||||
ADDON_MOD_H5PACTIVITY_TRACK_COMPONENT,
|
ADDON_MOD_H5PACTIVITY_TRACK_COMPONENT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { CoreH5PMissingDependenciesError } from '@features/h5p/classes/errors/missing-dependencies-error';
|
||||||
|
import { CoreToasts, ToastDuration } from '@services/toasts';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { NgZone, Translate } from '@singletons';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays an H5P activity entry page.
|
* Component that displays an H5P activity entry page.
|
||||||
|
@ -89,8 +94,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
isOpeningPage = false;
|
isOpeningPage = false;
|
||||||
canViewAllAttempts = false;
|
canViewAllAttempts = false;
|
||||||
saveStateEnabled = false;
|
saveStateEnabled = false;
|
||||||
|
hasMissingDependencies = false;
|
||||||
saveFreq?: number;
|
saveFreq?: number;
|
||||||
contentState?: string;
|
contentState?: string;
|
||||||
|
isOnline: boolean;
|
||||||
|
triedToPlay = false;
|
||||||
|
|
||||||
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
|
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
|
||||||
protected syncEventName = ADDON_MOD_H5PACTIVITY_AUTO_SYNCED;
|
protected syncEventName = ADDON_MOD_H5PACTIVITY_AUTO_SYNCED;
|
||||||
|
@ -98,6 +106,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
protected observer?: CoreEventObserver;
|
protected observer?: CoreEventObserver;
|
||||||
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
|
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
|
||||||
protected checkCompletionAfterLog = false; // It's called later, when the user plays the package.
|
protected checkCompletionAfterLog = false; // It's called later, when the user plays the package.
|
||||||
|
protected onlineObserver: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected content?: IonContent,
|
protected content?: IonContent,
|
||||||
|
@ -111,6 +120,39 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
// Listen for messages from the iframe.
|
// Listen for messages from the iframe.
|
||||||
this.messageListenerFunction = (event) => this.onIframeMessage(event);
|
this.messageListenerFunction = (event) => this.onIframeMessage(event);
|
||||||
window.addEventListener('message', this.messageListenerFunction);
|
window.addEventListener('message', this.messageListenerFunction);
|
||||||
|
|
||||||
|
this.isOnline = CoreNetwork.isOnline();
|
||||||
|
this.onlineObserver = CoreNetwork.onChange().subscribe(() => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.run(() => {
|
||||||
|
this.networkChanged();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React to a network status change.
|
||||||
|
*/
|
||||||
|
protected networkChanged(): void {
|
||||||
|
const wasOnline = this.isOnline;
|
||||||
|
this.isOnline = CoreNetwork.isOnline();
|
||||||
|
|
||||||
|
if (this.playing && !this.fileUrl && !this.isOnline && wasOnline && this.trackComponent) {
|
||||||
|
// User lost connection while playing an online package with tracking. Show an error.
|
||||||
|
CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'), {
|
||||||
|
title: Translate.instant('core.youreoffline'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isOnline && this.triedToPlay) {
|
||||||
|
// User couldn't play the package because he was offline, but he reconnected. Try again.
|
||||||
|
this.triedToPlay = false;
|
||||||
|
this.play();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,7 +206,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.siteCanDownload || this.state === DownloadStatus.DOWNLOADED) {
|
if (!this.siteCanDownload || this.state === DownloadStatus.DOWNLOADED || this.hasMissingDependencies) {
|
||||||
// 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();
|
||||||
|
|
||||||
|
@ -219,12 +261,18 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity, {
|
const deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity, {
|
||||||
displayOptions: this.displayOptions,
|
displayOptions: this.displayOptions,
|
||||||
siteId: this.siteId,
|
siteId: this.siteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fileUrl = CoreFileHelper.getFileUrl(this.deployedFile);
|
this.hasMissingDependencies = await AddonModH5PActivity.hasMissingDependencies(this.module.id, deployedFile);
|
||||||
|
if (this.hasMissingDependencies) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deployedFile = deployedFile;
|
||||||
|
this.fileUrl = CoreFileHelper.getFileUrl(deployedFile);
|
||||||
|
|
||||||
// Listen for changes in the state.
|
// Listen for changes in the state.
|
||||||
const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.fileUrl);
|
const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.fileUrl);
|
||||||
|
@ -362,6 +410,20 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof CoreH5PMissingDependenciesError) {
|
||||||
|
// Cannot be played offline, use online player.
|
||||||
|
this.hasMissingDependencies = true;
|
||||||
|
this.fileUrl = undefined;
|
||||||
|
this.play();
|
||||||
|
|
||||||
|
CoreToasts.show({
|
||||||
|
message: Translate.instant('core.course.activityrequiresconnection'),
|
||||||
|
duration: ToastDuration.LONG,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -448,6 +510,16 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.fileUrl && !this.isOnline) {
|
||||||
|
this.triedToPlay = true;
|
||||||
|
|
||||||
|
CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.connectandtryagain'), {
|
||||||
|
title: Translate.instant('core.course.activitynotavailableoffline'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
|
|
||||||
// Mark the activity as viewed.
|
// Mark the activity as viewed.
|
||||||
|
@ -456,6 +528,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
this.checkCompletion();
|
this.checkCompletion();
|
||||||
|
|
||||||
this.analyticsLogEvent('mod_h5pactivity_view_h5pactivity');
|
this.analyticsLogEvent('mod_h5pactivity_view_h5pactivity');
|
||||||
|
|
||||||
|
if (!this.fileUrl && this.trackComponent) {
|
||||||
|
// User is playing the package in online, invalidate attempts to fetch latest data.
|
||||||
|
AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, CoreSites.getCurrentSiteUserId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -466,6 +543,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
const userId = CoreSites.getCurrentSiteUserId();
|
const userId = CoreSites.getCurrentSiteUserId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!this.fileUrl && this.trackComponent && this.h5pActivity) {
|
||||||
|
// User is playing the package in online, invalidate attempts to fetch latest data.
|
||||||
|
await AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, CoreSites.getCurrentSiteUserId());
|
||||||
|
}
|
||||||
|
|
||||||
await CoreNavigator.navigateToSitePath(
|
await CoreNavigator.navigateToSitePath(
|
||||||
`${ADDON_MOD_H5PACTIVITY_PAGE_NAME}/${this.courseId}/${this.module.id}/userattempts/${userId}`,
|
`${ADDON_MOD_H5PACTIVITY_PAGE_NAME}/${this.courseId}/${this.module.id}/userattempts/${userId}`,
|
||||||
);
|
);
|
||||||
|
@ -732,6 +814,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
|
||||||
super.ngOnDestroy();
|
super.ngOnDestroy();
|
||||||
|
|
||||||
this.observer?.off();
|
this.observer?.off();
|
||||||
|
this.onlineObserver.unsubscribe();
|
||||||
|
|
||||||
// Wait a bit to make sure all messages have been received.
|
// Wait a bit to make sure all messages have been received.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -32,6 +32,9 @@ import {
|
||||||
AddonModH5PActivityGradeMethod,
|
AddonModH5PActivityGradeMethod,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { CoreCacheUpdateFrequency } from '@/core/constants';
|
import { CoreCacheUpdateFrequency } from '@/core/constants';
|
||||||
|
import { CoreFileHelper } from '@services/file-helper';
|
||||||
|
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||||
|
import { CoreH5PMissingDependencyDBRecord } from '@features/h5p/services/database/h5p';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features for H5P activity.
|
* Service that provides some features for H5P activity.
|
||||||
|
@ -571,6 +574,45 @@ export class AddonModH5PActivityProvider {
|
||||||
return this.getH5PActivityByField(courseId, 'id', id, options);
|
return this.getH5PActivityByField(courseId, 'id', id, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get missing dependencies for a certain H5P activity.
|
||||||
|
*
|
||||||
|
* @param componentId Component ID.
|
||||||
|
* @param deployedFile File to check.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Missing dependencies, empty if no missing dependencies.
|
||||||
|
*/
|
||||||
|
async getMissingDependencies(
|
||||||
|
componentId: number,
|
||||||
|
deployedFile: CoreWSFile,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<CoreH5PMissingDependencyDBRecord[]> {
|
||||||
|
const fileUrl = CoreFileHelper.getFileUrl(deployedFile);
|
||||||
|
|
||||||
|
const missingDependencies =
|
||||||
|
await CoreH5P.h5pFramework.getMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, componentId, siteId);
|
||||||
|
if (!missingDependencies.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The activity had missing dependencies, but the package could have changed (e.g. the teacher fixed it).
|
||||||
|
// Check which of the dependencies apply to the current package.
|
||||||
|
const fileId = await CoreH5P.h5pFramework.getFileIdForMissingDependencies(fileUrl, siteId);
|
||||||
|
|
||||||
|
const filteredMissingDependencies = missingDependencies.filter(dependency =>
|
||||||
|
dependency.fileid === fileId && dependency.filetimemodified === deployedFile.timemodified);
|
||||||
|
if (filteredMissingDependencies.length > 0) {
|
||||||
|
return filteredMissingDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package has changed, delete previous missing dependencies.
|
||||||
|
await CorePromiseUtils.ignoreErrors(
|
||||||
|
CoreH5P.h5pFramework.deleteMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, componentId, siteId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cache key for attemps WS calls.
|
* Get cache key for attemps WS calls.
|
||||||
*
|
*
|
||||||
|
@ -658,6 +700,20 @@ export class AddonModH5PActivityProvider {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a package has missing dependencies.
|
||||||
|
*
|
||||||
|
* @param componentId Component ID.
|
||||||
|
* @param deployedFile File to check.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Whether the package has missing dependencies.
|
||||||
|
*/
|
||||||
|
async hasMissingDependencies(componentId: number, deployedFile: CoreWSFile, siteId?: string): Promise<boolean> {
|
||||||
|
const missingDependencies = await this.getMissingDependencies(componentId, deployedFile, siteId);
|
||||||
|
|
||||||
|
return missingDependencies.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidates access information.
|
* Invalidates access information.
|
||||||
*
|
*
|
||||||
|
|
|
@ -144,6 +144,12 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we already detected that the file has missing dependencies there's no need to download it again.
|
||||||
|
const missingDependencies = await AddonModH5PActivity.getMissingDependencies(module.id, deployedFile, siteId);
|
||||||
|
if (missingDependencies.length > 0) {
|
||||||
|
throw CoreH5P.h5pFramework.buildMissingDependenciesErrorFromDBRecords(missingDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
if (AddonModH5PActivity.isSaveStateEnabled(h5pActivity)) {
|
if (AddonModH5PActivity.isSaveStateEnabled(h5pActivity)) {
|
||||||
// If the file needs to be downloaded, delete the states because it means the package has changed or user deleted it.
|
// If the file needs to be downloaded, delete the states because it means the package has changed or user deleted it.
|
||||||
const fileState = await CoreFilepool.getFileStateByUrl(siteId, CoreFileHelper.getFileUrl(deployedFile));
|
const fileState = await CoreFilepool.getFileStateByUrl(siteId, CoreFileHelper.getFileUrl(deployedFile));
|
||||||
|
@ -254,6 +260,19 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async removeFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
|
||||||
|
// Remove files and delete any missing dependency stored to force recalculating them.
|
||||||
|
await Promise.all([
|
||||||
|
super.removeFiles(module, courseId),
|
||||||
|
CorePromiseUtils.ignoreErrors(
|
||||||
|
CoreH5P.h5pFramework.deleteMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, module.id),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddonModH5PActivityPrefetchHandler = makeSingleton(AddonModH5PActivityPrefetchHandlerService);
|
export const AddonModH5PActivityPrefetchHandler = makeSingleton(AddonModH5PActivityPrefetchHandlerService);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
{
|
{
|
||||||
"activitydisabled": "Your organisation has disabled this activity in the mobile app.",
|
"activitydisabled": "Your organisation has disabled this activity in the mobile app.",
|
||||||
|
"activitynotavailableoffline": "This activity is not available offline.",
|
||||||
"activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.",
|
"activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.",
|
||||||
|
"activityrequiresconnection": "This activity is only available with an internet connection. If your device is offline, you will not be able to access it.",
|
||||||
"allsections": "All sections",
|
"allsections": "All sections",
|
||||||
"aria:sectionprogress": "Section progress:",
|
"aria:sectionprogress": "Section progress:",
|
||||||
"availablespace": "You currently have about {{available}} free space.",
|
"availablespace": "You currently have about {{available}} free space.",
|
||||||
"cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.",
|
"cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.",
|
||||||
|
"changesofflinemaybelost": "Any changes you make to this activity while offline may not be saved.<br><br>Connect your device to the internet to avoid losing your progress.",
|
||||||
"communicationroomlink": "Chat to course participants",
|
"communicationroomlink": "Chat to course participants",
|
||||||
"completion_automatic:done": "Done:",
|
"completion_automatic:done": "Done:",
|
||||||
"completion_automatic:failed": "Failed:",
|
"completion_automatic:failed": "Failed:",
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// (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 { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreH5PMissingLibrary } from '../core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Missing dependencies error when deploying an H5P package.
|
||||||
|
*/
|
||||||
|
export class CoreH5PMissingDependenciesError extends CoreError {
|
||||||
|
|
||||||
|
missingDependencies: CoreH5PMissingLibrary[];
|
||||||
|
|
||||||
|
constructor(message: string, missingDependencies: CoreH5PMissingLibrary[]) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
this.missingDependencies = missingDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import {
|
||||||
CoreH5PContentDepsTreeDependency,
|
CoreH5PContentDepsTreeDependency,
|
||||||
CoreH5PLibraryBasicData,
|
CoreH5PLibraryBasicData,
|
||||||
CoreH5PLibraryBasicDataWithPatch,
|
CoreH5PLibraryBasicDataWithPatch,
|
||||||
|
CoreH5PMissingLibrary,
|
||||||
} from './core';
|
} from './core';
|
||||||
import {
|
import {
|
||||||
CONTENT_TABLE_NAME,
|
CONTENT_TABLE_NAME,
|
||||||
|
@ -36,6 +37,10 @@ import {
|
||||||
CoreH5PLibraryDBRecord,
|
CoreH5PLibraryDBRecord,
|
||||||
CoreH5PLibraryDependencyDBRecord,
|
CoreH5PLibraryDependencyDBRecord,
|
||||||
CoreH5PContentsLibraryDBRecord,
|
CoreH5PContentsLibraryDBRecord,
|
||||||
|
CoreH5PMissingDependencyDBRecord,
|
||||||
|
MISSING_DEPENDENCIES_TABLE_NAME,
|
||||||
|
MISSING_DEPENDENCIES_PRIMARY_KEYS,
|
||||||
|
CoreH5PMissingDependencyDBPrimaryKeys,
|
||||||
} from '../services/database/h5p';
|
} from '../services/database/h5p';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreH5PSemantics } from './content-validator';
|
import { CoreH5PSemantics } from './content-validator';
|
||||||
|
@ -48,6 +53,11 @@ import { LazyMap, lazyMap } from '@/core/utils/lazy-map';
|
||||||
import { CoreDatabaseTable } from '@classes/database/database-table';
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
||||||
import { SubPartial } from '@/core/utils/types';
|
import { SubPartial } from '@/core/utils/types';
|
||||||
|
import { CoreH5PMissingDependenciesError } from './errors/missing-dependencies-error';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreFileHelper } from '@services/file-helper';
|
||||||
|
import { CoreUrl, CoreUrlPartNames } from '@singletons/url';
|
||||||
|
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Equivalent to Moodle's implementation of H5PFrameworkInterface.
|
* Equivalent to Moodle's implementation of H5PFrameworkInterface.
|
||||||
|
@ -59,6 +69,9 @@ export class CoreH5PFramework {
|
||||||
protected libraryDependenciesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PLibraryDependencyDBRecord>>>;
|
protected libraryDependenciesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PLibraryDependencyDBRecord>>>;
|
||||||
protected contentsLibrariesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PContentsLibraryDBRecord>>>;
|
protected contentsLibrariesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PContentsLibraryDBRecord>>>;
|
||||||
protected librariesCachedAssetsTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PLibraryCachedAssetsDBRecord>>>;
|
protected librariesCachedAssetsTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreH5PLibraryCachedAssetsDBRecord>>>;
|
||||||
|
protected missingDependenciesTables: LazyMap<
|
||||||
|
AsyncInstance<CoreDatabaseTable<CoreH5PMissingDependencyDBRecord, CoreH5PMissingDependencyDBPrimaryKeys>>
|
||||||
|
>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.contentTables = lazyMap(
|
this.contentTables = lazyMap(
|
||||||
|
@ -121,6 +134,43 @@ export class CoreH5PFramework {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
this.missingDependenciesTables = lazyMap(
|
||||||
|
siteId => asyncInstance(
|
||||||
|
() => CoreSites.getSiteTable(
|
||||||
|
MISSING_DEPENDENCIES_TABLE_NAME,
|
||||||
|
{
|
||||||
|
siteId,
|
||||||
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
||||||
|
onDestroy: () => delete this.missingDependenciesTables[siteId],
|
||||||
|
primaryKeyColumns: [...MISSING_DEPENDENCIES_PRIMARY_KEYS],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of missing dependencies DB records, create a missing dependencies error.
|
||||||
|
*
|
||||||
|
* @param missingDependencies List of missing dependencies.
|
||||||
|
* @returns Error instance.
|
||||||
|
*/
|
||||||
|
buildMissingDependenciesErrorFromDBRecords(
|
||||||
|
missingDependencies: CoreH5PMissingDependencyDBRecord[],
|
||||||
|
): CoreH5PMissingDependenciesError {
|
||||||
|
const missingLibraries = missingDependencies.map(dep => ({
|
||||||
|
machineName: dep.machinename,
|
||||||
|
majorVersion: dep.majorversion,
|
||||||
|
minorVersion: dep.minorversion,
|
||||||
|
libString: dep.requiredby,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const errorMessage = Translate.instant('core.h5p.missingdependency', { $a: {
|
||||||
|
lib: missingLibraries[0].libString,
|
||||||
|
dep: CoreH5PCore.libraryToString(missingLibraries[0]),
|
||||||
|
} });
|
||||||
|
|
||||||
|
return new CoreH5PMissingDependenciesError(errorMessage, missingLibraries);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -236,6 +286,33 @@ export class CoreH5PFramework {
|
||||||
await this.contentsLibrariesTables[siteId].delete({ h5pid: id });
|
await this.contentsLibrariesTables[siteId].delete({ h5pid: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete missing dependencies stored for a certain component and componentId.
|
||||||
|
*
|
||||||
|
* @param component Component.
|
||||||
|
* @param componentId Component ID.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
*/
|
||||||
|
async deleteMissingDependenciesForComponent(component: string, componentId: string | number, siteId?: string): Promise<void> {
|
||||||
|
siteId ??= CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
await this.missingDependenciesTables[siteId].delete({ component, componentId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all the missing dependencies related to a certain library version.
|
||||||
|
*
|
||||||
|
* @param libraryData Library.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
*/
|
||||||
|
protected async deleteMissingDependenciesForLibrary(libraryData: CoreH5PLibraryBasicData, siteId: string): Promise<void> {
|
||||||
|
await this.missingDependenciesTables[siteId].delete({
|
||||||
|
machinename: libraryData.machineName,
|
||||||
|
majorversion: libraryData.majorVersion,
|
||||||
|
minorversion: libraryData.minorVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all conent data from DB.
|
* Get all conent data from DB.
|
||||||
*
|
*
|
||||||
|
@ -282,6 +359,38 @@ export class CoreH5PFramework {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an identifier for a file URL, used to store missing dependencies.
|
||||||
|
*
|
||||||
|
* @param fileUrl File URL.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns An identifier for the file.
|
||||||
|
*/
|
||||||
|
async getFileIdForMissingDependencies(fileUrl: string, siteId?: string): Promise<string> {
|
||||||
|
siteId ??= CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const isTrusted = await CoreH5P.isTrustedUrl(fileUrl, siteId);
|
||||||
|
if (!isTrusted) {
|
||||||
|
// Fix the URL, we need to URL of the trusted package.
|
||||||
|
const file = await CoreFilepool.fixPluginfileURL(siteId, fileUrl);
|
||||||
|
|
||||||
|
fileUrl = CoreFileHelper.getFileUrl(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all params from the URL except the time modified. We don't want the id to depend on changing params like
|
||||||
|
// the language or the token.
|
||||||
|
const urlParams = CoreUrl.extractUrlParams(fileUrl);
|
||||||
|
fileUrl = CoreUrl.addParamsToUrl(
|
||||||
|
CoreUrl.removeUrlParts(fileUrl, [CoreUrlPartNames.Query]),
|
||||||
|
{ modified: urlParams.modified },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only return the file args, that way the id doesn't depend on the endpoint to obtain the file.
|
||||||
|
const fileArgs = CoreUrl.getPluginFileArgs(fileUrl);
|
||||||
|
|
||||||
|
return fileArgs ? fileArgs.join('/') : fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the latest library version.
|
* Get the latest library version.
|
||||||
*
|
*
|
||||||
|
@ -405,6 +514,47 @@ export class CoreH5PFramework {
|
||||||
return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId);
|
return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get missing dependencies stored for a certain component and componentId.
|
||||||
|
*
|
||||||
|
* @param component Component.
|
||||||
|
* @param componentId Component ID.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @returns List of missing dependencies. Empty list if no missing dependencies stored for the file.
|
||||||
|
*/
|
||||||
|
async getMissingDependenciesForComponent(
|
||||||
|
component: string,
|
||||||
|
componentId: string | number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<CoreH5PMissingDependencyDBRecord[]> {
|
||||||
|
siteId ??= CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.missingDependenciesTables[siteId].getMany({ component, componentId });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get missing dependencies stored for a certain file.
|
||||||
|
*
|
||||||
|
* @param fileUrl File URL.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @returns List of missing dependencies. Empty list if no missing dependencies stored for the file.
|
||||||
|
*/
|
||||||
|
async getMissingDependenciesForFile(fileUrl: string, siteId?: string): Promise<CoreH5PMissingDependencyDBRecord[]> {
|
||||||
|
siteId ??= CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileId = await this.getFileIdForMissingDependencies(fileUrl, siteId);
|
||||||
|
|
||||||
|
return await this.missingDependenciesTables[siteId].getMany({ fileid: fileId });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default behaviour for the display option defined.
|
* Get the default behaviour for the display option defined.
|
||||||
*
|
*
|
||||||
|
@ -812,6 +962,9 @@ export class CoreH5PFramework {
|
||||||
// Updated libary. Remove old dependencies.
|
// Updated libary. Remove old dependencies.
|
||||||
await this.deleteLibraryDependencies(data.id, siteId);
|
await this.deleteLibraryDependencies(data.id, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete missing dependencies related to this library. Don't block the execution for this.
|
||||||
|
CorePromiseUtils.ignoreErrors(this.deleteMissingDependenciesForLibrary(libraryData, siteId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -830,6 +983,7 @@ export class CoreH5PFramework {
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const targetSiteId = siteId ?? CoreSites.getCurrentSiteId();
|
const targetSiteId = siteId ?? CoreSites.getCurrentSiteId();
|
||||||
|
const libString = CoreH5PCore.libraryToString(library);
|
||||||
|
|
||||||
await Promise.all(dependencies.map(async (dependency) => {
|
await Promise.all(dependencies.map(async (dependency) => {
|
||||||
// Get the ID of the library.
|
// Get the ID of the library.
|
||||||
|
@ -837,10 +991,10 @@ export class CoreH5PFramework {
|
||||||
|
|
||||||
if (!dependencyId) {
|
if (!dependencyId) {
|
||||||
// Missing dependency. It should have been detected before installing the package.
|
// Missing dependency. It should have been detected before installing the package.
|
||||||
throw new CoreError(Translate.instant('core.h5p.missingdependency', { $a: {
|
throw new CoreH5PMissingDependenciesError(Translate.instant('core.h5p.missingdependency', { $a: {
|
||||||
lib: CoreH5PCore.libraryToString(library),
|
lib: CoreH5PCore.libraryToString(library),
|
||||||
dep: CoreH5PCore.libraryToString(dependency),
|
dep: CoreH5PCore.libraryToString(dependency),
|
||||||
} }));
|
} }), [{ ...dependency, libString }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the relation.
|
// Create the relation.
|
||||||
|
@ -900,6 +1054,34 @@ export class CoreH5PFramework {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store missing dependencies in DB.
|
||||||
|
*
|
||||||
|
* @param fileUrl URL of the package that has missing dependencies.
|
||||||
|
* @param missingDependencies List of missing dependencies.
|
||||||
|
* @param options Other options.
|
||||||
|
*/
|
||||||
|
async storeMissingDependencies(
|
||||||
|
fileUrl: string,
|
||||||
|
missingDependencies: CoreH5PMissingLibrary[],
|
||||||
|
options: StoreMissingDependenciesOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const targetSiteId = options.siteId ?? CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const fileId = await this.getFileIdForMissingDependencies(fileUrl, targetSiteId);
|
||||||
|
|
||||||
|
await Promise.all(missingDependencies.map((missingLibrary) => this.missingDependenciesTables[targetSiteId].insert({
|
||||||
|
fileid: fileId,
|
||||||
|
machinename: missingLibrary.machineName,
|
||||||
|
majorversion: missingLibrary.majorVersion,
|
||||||
|
minorversion: missingLibrary.minorVersion,
|
||||||
|
requiredby: missingLibrary.libString,
|
||||||
|
filetimemodified: options.fileTimemodified ?? 0,
|
||||||
|
component: options.component,
|
||||||
|
componentId: options.componentId,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save content data in DB and clear cache.
|
* Save content data in DB and clear cache.
|
||||||
*
|
*
|
||||||
|
@ -1011,3 +1193,13 @@ type LibraryDependency = {
|
||||||
type LibraryAddonDBData = Omit<CoreH5PLibraryAddonData, 'addTo'> & {
|
type LibraryAddonDBData = Omit<CoreH5PLibraryAddonData, 'addTo'> & {
|
||||||
addTo: string;
|
addTo: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for storeMissingDependencies.
|
||||||
|
*/
|
||||||
|
type StoreMissingDependenciesOptions = {
|
||||||
|
component?: string;
|
||||||
|
componentId?: string | number;
|
||||||
|
fileTimemodified?: number;
|
||||||
|
siteId?: string;
|
||||||
|
};
|
||||||
|
|
|
@ -21,6 +21,9 @@ import { CoreH5P } from '../services/h5p';
|
||||||
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PLocalization } from './core';
|
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PLocalization } from './core';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CorePath } from '@singletons/path';
|
import { CorePath } from '@singletons/path';
|
||||||
|
import { CorePluginFileTreatDownloadedFileOptions } from '@services/plugin-file-delegate';
|
||||||
|
import { CoreH5PMissingDependenciesError } from './errors/missing-dependencies-error';
|
||||||
|
import { CorePromiseUtils } from '@singletons/promise-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Equivalent to Moodle's H5P helper class.
|
* Equivalent to Moodle's H5P helper class.
|
||||||
|
@ -89,13 +92,13 @@ export class CoreH5PHelper {
|
||||||
|
|
||||||
// Add core stylesheets.
|
// Add core stylesheets.
|
||||||
CoreH5PCore.STYLES.forEach((style) => {
|
CoreH5PCore.STYLES.forEach((style) => {
|
||||||
settings.core!.styles.push(libUrl + style);
|
settings.core?.styles.push(libUrl + style);
|
||||||
cssRequires.push(libUrl + style);
|
cssRequires.push(libUrl + style);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add core JavaScript.
|
// Add core JavaScript.
|
||||||
CoreH5PCore.getScripts().forEach((script) => {
|
CoreH5PCore.getScripts().forEach((script) => {
|
||||||
settings.core!.scripts.push(script);
|
settings.core?.scripts.push(script);
|
||||||
jsRequires.push(script);
|
jsRequires.push(script);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -163,19 +166,22 @@ export class CoreH5PHelper {
|
||||||
*
|
*
|
||||||
* @param fileUrl The file URL used to download the file.
|
* @param fileUrl The file URL used to download the file.
|
||||||
* @param file The file entry of the downloaded file.
|
* @param file The file entry of the downloaded file.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param options Options.
|
||||||
* @param onProgress Function to call on progress.
|
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
static async saveH5P(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreH5PSaveOnProgress): Promise<void> {
|
static async saveH5P(
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
fileUrl: string,
|
||||||
|
file: FileEntry,
|
||||||
|
options: CorePluginFileTreatDownloadedFileOptions<ProgressEvent | { message: string }> = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const siteId = options.siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
// Notify that the unzip is starting.
|
// Notify that the unzip is starting.
|
||||||
onProgress && onProgress({ message: 'core.unzipping' });
|
options.onProgress && options.onProgress({ message: 'core.unzipping' });
|
||||||
|
|
||||||
const queueId = siteId + ':saveH5P:' + fileUrl;
|
const queueId = siteId + ':saveH5P:' + fileUrl;
|
||||||
|
|
||||||
await CoreH5P.queueRunner.run(queueId, () => CoreH5PHelper.performSave(fileUrl, file, siteId, onProgress));
|
await CoreH5P.queueRunner.run(queueId, () => CoreH5PHelper.performSave(fileUrl, file, { ...options, siteId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,40 +189,54 @@ export class CoreH5PHelper {
|
||||||
*
|
*
|
||||||
* @param fileUrl The file URL used to download the file.
|
* @param fileUrl The file URL used to download the file.
|
||||||
* @param file The file entry of the downloaded file.
|
* @param file The file entry of the downloaded file.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param options Options.
|
||||||
* @param onProgress Function to call on progress.
|
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected static async performSave(
|
protected static async performSave(
|
||||||
fileUrl: string,
|
fileUrl: string,
|
||||||
file: FileEntry,
|
file: FileEntry,
|
||||||
siteId?: string,
|
options: CorePluginFileTreatDownloadedFileOptions<ProgressEvent | { message: string }> = {},
|
||||||
onProgress?: CoreH5PSaveOnProgress,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
||||||
const folderName = CoreMimetypeUtils.removeExtension(file.name);
|
const folderName = CoreMimetypeUtils.removeExtension(file.name);
|
||||||
const destFolder = CorePath.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName);
|
const destFolder = CorePath.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName);
|
||||||
|
|
||||||
// Unzip the file.
|
// Unzip the file.
|
||||||
await CoreFile.unzipFile(CoreFile.getFileEntryURL(file), destFolder, onProgress);
|
await CoreFile.unzipFile(CoreFile.getFileEntryURL(file), destFolder, options.onProgress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Notify that the unzip is starting.
|
// Notify that the unzip is starting.
|
||||||
onProgress && onProgress({ message: 'core.storingfiles' });
|
options.onProgress && options.onProgress({ message: 'core.storingfiles' });
|
||||||
|
|
||||||
// Read the contents of the unzipped dir, process them and store them.
|
// Read the contents of the unzipped dir, process them and store them.
|
||||||
const contents = await CoreFile.getDirectoryContents(destFolder);
|
const contents = await CoreFile.getDirectoryContents(destFolder);
|
||||||
|
|
||||||
const filesData = await CoreH5P.h5pValidator.processH5PFiles(destFolder, contents, siteId);
|
const filesData = await CoreH5P.h5pValidator.processH5PFiles(destFolder, contents, options.siteId);
|
||||||
|
|
||||||
const content = await CoreH5P.h5pStorage.savePackage(filesData, folderName, fileUrl, false, siteId);
|
const content = await CoreH5P.h5pStorage.savePackage(filesData, folderName, fileUrl, false, options.siteId);
|
||||||
|
|
||||||
// Create the content player.
|
// Create the content player.
|
||||||
const contentData = await CoreH5P.h5pCore.loadContent(content.id, undefined, siteId);
|
const contentData = await CoreH5P.h5pCore.loadContent(content.id, undefined, options.siteId);
|
||||||
|
|
||||||
const embedType = CoreH5PCore.determineEmbedType(contentData.embedType, contentData.library.embedTypes);
|
const embedType = CoreH5PCore.determineEmbedType(contentData.embedType, contentData.library.embedTypes);
|
||||||
|
|
||||||
await CoreH5P.h5pPlayer.createContentIndex(content.id!, fileUrl, contentData, embedType, siteId);
|
await CoreH5P.h5pPlayer.createContentIndex(content.id!, fileUrl, contentData, embedType, options.siteId);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof CoreH5PMissingDependenciesError) {
|
||||||
|
// Store the missing dependencies to avoid re-downloading the file every time.
|
||||||
|
await CorePromiseUtils.ignoreErrors(CoreH5P.h5pFramework.storeMissingDependencies(
|
||||||
|
fileUrl,
|
||||||
|
error.missingDependencies,
|
||||||
|
{
|
||||||
|
component: options.component,
|
||||||
|
componentId: options.componentId,
|
||||||
|
fileTimemodified: options.timemodified,
|
||||||
|
siteId: options.siteId,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Remove tmp folder.
|
// Remove tmp folder.
|
||||||
try {
|
try {
|
||||||
|
@ -264,5 +284,3 @@ export type CoreH5PCoreSettings = {
|
||||||
loadedJs?: string[];
|
loadedJs?: string[];
|
||||||
loadedCss?: string[];
|
loadedCss?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CoreH5PSaveOnProgress = (event?: ProgressEvent | { message: string }) => void;
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreH5PMissingDependenciesError } from './errors/missing-dependencies-error';
|
||||||
import { FileEntry, DirectoryEntry } from '@awesome-cordova-plugins/file/ngx';
|
import { FileEntry, DirectoryEntry } from '@awesome-cordova-plugins/file/ngx';
|
||||||
import { CoreFile, CoreFileFormat } from '@services/file';
|
import { CoreFile, CoreFileFormat } from '@services/file';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
|
@ -269,10 +269,10 @@ export class CoreH5PValidator {
|
||||||
const libString = Object.keys(missingLibraries)[0];
|
const libString = Object.keys(missingLibraries)[0];
|
||||||
const missingLibrary = missingLibraries[libString];
|
const missingLibrary = missingLibraries[libString];
|
||||||
|
|
||||||
throw new CoreError(Translate.instant('core.h5p.missingdependency', { $a: {
|
throw new CoreH5PMissingDependenciesError(Translate.instant('core.h5p.missingdependency', { $a: {
|
||||||
lib: missingLibrary.libString,
|
lib: missingLibrary.libString,
|
||||||
dep: libString,
|
dep: libString,
|
||||||
} }));
|
} }), Object.values(missingLibraries));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { librariesJsonData, mainJsonData, contentJsonData };
|
return { librariesJsonData, mainJsonData, contentJsonData };
|
||||||
|
|
|
@ -49,6 +49,9 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
||||||
@Input({ transform: toBoolean }) enableInAppFullscreen = false; // Whether to enable our custom in-app fullscreen feature.
|
@Input({ transform: toBoolean }) enableInAppFullscreen = false; // Whether to enable our custom in-app fullscreen feature.
|
||||||
@Input() saveFreq?: number; // Save frequency (in seconds) if enabled.
|
@Input() saveFreq?: number; // Save frequency (in seconds) if enabled.
|
||||||
@Input() state?: string; // Initial content state.
|
@Input() state?: string; // Initial content state.
|
||||||
|
@Input() component?: string; // Component the file is linked to.
|
||||||
|
@Input() componentId?: string | number; // Component ID.
|
||||||
|
@Input() fileTimemodified?: number; // The timemodified of the file.
|
||||||
@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>();
|
||||||
|
|
||||||
|
@ -181,7 +184,12 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
|
||||||
|
|
||||||
const file = await CoreFile.getFile(path);
|
const file = await CoreFile.getFile(path);
|
||||||
|
|
||||||
await CoreH5PHelper.saveH5P(this.fileUrl!, file, this.siteId);
|
await CoreH5PHelper.saveH5P(this.fileUrl!, file, {
|
||||||
|
siteId: this.siteId,
|
||||||
|
component: this.component,
|
||||||
|
componentId: this.componentId,
|
||||||
|
timemodified: this.fileTimemodified,
|
||||||
|
});
|
||||||
|
|
||||||
// File treated. Try to get the index file URL again.
|
// File treated. Try to get the index file URL again.
|
||||||
const url = await CoreH5P.h5pPlayer.getContentIndexFileUrl(
|
const url = await CoreH5P.h5pPlayer.getContentIndexFileUrl(
|
||||||
|
|
|
@ -9,4 +9,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<core-h5p-iframe *ngIf="showPackage" [fileUrl]="urlParams!.url" [displayOptions]="displayOptions" [onlinePlayerUrl]="src" />
|
<core-h5p-iframe *ngIf="showPackage" [fileUrl]="urlParams!.url" [displayOptions]="displayOptions" [onlinePlayerUrl]="src"
|
||||||
|
[component]="component" [componentId]="componentId" [fileTimemodified]="fileTimemodified" />
|
||||||
|
|
|
@ -41,6 +41,7 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() src?: string; // The URL of the player to display the H5P package.
|
@Input() src?: string; // The URL of the player to display the H5P package.
|
||||||
@Input() component?: string; // Component.
|
@Input() component?: string; // Component.
|
||||||
@Input() componentId?: string | number; // Component ID to use in conjunction with the component.
|
@Input() componentId?: string | number; // Component ID to use in conjunction with the component.
|
||||||
|
@Input() fileTimemodified?: number; // The timemodified of the package file.
|
||||||
|
|
||||||
showPackage = false;
|
showPackage = false;
|
||||||
state?: string;
|
state?: string;
|
||||||
|
@ -122,6 +123,12 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if the package has missing dependencies. If so, it cannot be downloaded.
|
||||||
|
const missingDependencies = await CoreH5P.h5pFramework.getMissingDependenciesForFile(this.urlParams.url);
|
||||||
|
if (missingDependencies.length > 0) {
|
||||||
|
throw CoreH5P.h5pFramework.buildMissingDependenciesErrorFromDBRecords(missingDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the file size and ask the user to confirm.
|
// Get the file size and ask the user to confirm.
|
||||||
const size = await CorePluginFileDelegate.getFileSize({ fileurl: this.urlParams.url }, this.siteId);
|
const size = await CorePluginFileDelegate.getFileSize({ fileurl: this.urlParams.url }, this.siteId);
|
||||||
|
|
||||||
|
@ -152,6 +159,12 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the package has missing dependencies. If so, it cannot be downloaded.
|
||||||
|
const missingDependencies = await CoreH5P.h5pFramework.getMissingDependenciesForFile(this.urlParams.url);
|
||||||
|
if (missingDependencies.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the file size.
|
// Get the file size.
|
||||||
const size = await CorePluginFileDelegate.getFileSize({ fileurl: this.urlParams.url }, this.siteId);
|
const size = await CorePluginFileDelegate.getFileSize({ fileurl: this.urlParams.url }, this.siteId);
|
||||||
|
|
||||||
|
|
|
@ -24,15 +24,19 @@ export const LIBRARIES_TABLE_NAME = 'h5p_libraries'; // Installed libraries.
|
||||||
export const LIBRARY_DEPENDENCIES_TABLE_NAME = 'h5p_library_dependencies'; // Library dependencies.
|
export const LIBRARY_DEPENDENCIES_TABLE_NAME = 'h5p_library_dependencies'; // Library dependencies.
|
||||||
export const CONTENTS_LIBRARIES_TABLE_NAME = 'h5p_contents_libraries'; // Which library is used in which content.
|
export const CONTENTS_LIBRARIES_TABLE_NAME = 'h5p_contents_libraries'; // Which library is used in which content.
|
||||||
export const LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets.
|
export const LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets.
|
||||||
|
export const MISSING_DEPENDENCIES_TABLE_NAME = 'h5p_missing_dependencies'; // Information about missing dependencies.
|
||||||
|
export const MISSING_DEPENDENCIES_PRIMARY_KEYS = ['fileid', 'machinename', 'majorversion', 'minorversion'] as const;
|
||||||
|
|
||||||
export const SITE_SCHEMA: CoreSiteSchema = {
|
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
name: 'CoreH5PProvider',
|
name: 'CoreH5PProvider',
|
||||||
version: 2,
|
version: 3,
|
||||||
canBeCleared: [
|
canBeCleared: [
|
||||||
CONTENT_TABLE_NAME,
|
CONTENT_TABLE_NAME,
|
||||||
LIBRARIES_TABLE_NAME,
|
LIBRARIES_TABLE_NAME,
|
||||||
LIBRARY_DEPENDENCIES_TABLE_NAME,
|
LIBRARY_DEPENDENCIES_TABLE_NAME,
|
||||||
CONTENTS_LIBRARIES_TABLE_NAME,
|
CONTENTS_LIBRARIES_TABLE_NAME,
|
||||||
LIBRARIES_CACHEDASSETS_TABLE_NAME,
|
LIBRARIES_CACHEDASSETS_TABLE_NAME,
|
||||||
|
MISSING_DEPENDENCIES_TABLE_NAME,
|
||||||
],
|
],
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
|
@ -243,6 +247,46 @@ export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: MISSING_DEPENDENCIES_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'fileid',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'machinename',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'majorversion',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minorversion',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'requiredby',
|
||||||
|
type: 'TEXT',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filetimemodified',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'component',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'componentId',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: [...MISSING_DEPENDENCIES_PRIMARY_KEYS],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
|
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
|
||||||
if (oldVersion >= 2) {
|
if (oldVersion >= 2) {
|
||||||
|
@ -320,3 +364,19 @@ export type CoreH5PLibraryCachedAssetsDBRecord = {
|
||||||
hash: string; // The hash to identify the cached asset.
|
hash: string; // The hash to identify the cached asset.
|
||||||
foldername: string; // Name of the folder that contains the contents.
|
foldername: string; // Name of the folder that contains the contents.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of missing dependency data stored in DB.
|
||||||
|
*/
|
||||||
|
export type CoreH5PMissingDependencyDBRecord = {
|
||||||
|
fileid: string; // Identifier of the package that has a missing dependency. It will be part of the file url.
|
||||||
|
filetimemodified: number; // Time when the file was last modified.
|
||||||
|
machinename: string; // Machine name of the missing dependency.
|
||||||
|
majorversion: number; // Major version of the missing dependency.
|
||||||
|
minorversion: number; // Minor version of the missing dependency.
|
||||||
|
requiredby: string; // LibString of the library that requires the missing dependency.
|
||||||
|
component?: string; // Component related to the package.
|
||||||
|
componentId?: string | number; // Component ID related to the package.
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreH5PMissingDependencyDBPrimaryKeys = typeof MISSING_DEPENDENCIES_PRIMARY_KEYS[number];
|
||||||
|
|
|
@ -234,6 +234,19 @@ export class CoreH5PProvider {
|
||||||
return !!(site?.isOfflineDisabled() || site?.isFeatureDisabled('NoDelegate_H5POffline'));
|
return !!(site?.isOfflineDisabled() || site?.isFeatureDisabled('NoDelegate_H5POffline'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an H5P URL, check if it's a trusted URL.
|
||||||
|
*
|
||||||
|
* @param fileUrl File URL to check.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Whether it's a trusted URL.
|
||||||
|
*/
|
||||||
|
async isTrustedUrl(fileUrl: string, siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.containsUrl(fileUrl) && !!fileUrl.match(/pluginfile\.php\/([^/]+\/)?[^/]+\/core_h5p\/export\//i);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Treat an H5P url before sending it to WS.
|
* Treat an H5P url before sending it to WS.
|
||||||
*
|
*
|
||||||
|
|
|
@ -15,8 +15,11 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { FileEntry } from '@awesome-cordova-plugins/file/ngx';
|
import { FileEntry } from '@awesome-cordova-plugins/file/ngx';
|
||||||
|
|
||||||
import { CoreFilepoolOnProgressCallback } from '@services/filepool';
|
import {
|
||||||
import { CorePluginFileDownloadableResult, CorePluginFileHandler } from '@services/plugin-file-delegate';
|
CorePluginFileDownloadableResult,
|
||||||
|
CorePluginFileHandler,
|
||||||
|
CorePluginFileTreatDownloadedFileOptions,
|
||||||
|
} from '@services/plugin-file-delegate';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||||
import { CoreUrl } from '@singletons/url';
|
import { CoreUrl } from '@singletons/url';
|
||||||
|
@ -36,12 +39,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
||||||
name = 'CoreH5PPluginFileHandler';
|
name = 'CoreH5PPluginFileHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React to a file being deleted.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param fileUrl The file URL used to download the file.
|
|
||||||
* @param path The path of the deleted file.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
async fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<void> {
|
async fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<void> {
|
||||||
// If an h5p file is deleted, remove the contents folder.
|
// If an h5p file is deleted, remove the contents folder.
|
||||||
|
@ -49,18 +47,14 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a file can be downloaded. If so, return the file to download.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param file The file data.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @returns Promise resolved with the file to use. Rejected if cannot download.
|
|
||||||
*/
|
*/
|
||||||
async getDownloadableFile(file: CoreWSFile, siteId?: string): Promise<CoreWSFile> {
|
async getDownloadableFile(file: CoreWSFile, siteId?: string): Promise<CoreWSFile> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
const fileUrl = CoreFileHelper.getFileUrl(file);
|
const fileUrl = CoreFileHelper.getFileUrl(file);
|
||||||
|
|
||||||
if (site.containsUrl(fileUrl) && fileUrl.match(/pluginfile\.php\/[^/]+\/core_h5p\/export\//i)) {
|
const isTrusted = await CoreH5P.isTrustedUrl(fileUrl, siteId);
|
||||||
|
if (isTrusted) {
|
||||||
// It's already a deployed file, use it.
|
// It's already a deployed file, use it.
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
@ -69,11 +63,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an HTML element, get the URLs of the files that should be downloaded and weren't treated by
|
* @inheritdoc
|
||||||
* CoreFilepoolProvider.extractDownloadableFilesFromHtml.
|
|
||||||
*
|
|
||||||
* @param container Container where to get the URLs from.
|
|
||||||
* @returns List of URLs.
|
|
||||||
*/
|
*/
|
||||||
getDownloadableFilesFromHTML(container: HTMLElement): string[] {
|
getDownloadableFilesFromHTML(container: HTMLElement): string[] {
|
||||||
const iframes = <HTMLIFrameElement[]> Array.from(container.querySelectorAll('iframe.h5p-iframe'));
|
const iframes = <HTMLIFrameElement[]> Array.from(container.querySelectorAll('iframe.h5p-iframe'));
|
||||||
|
@ -91,11 +81,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a file size.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param file The file data.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @returns Promise resolved with the size.
|
|
||||||
*/
|
*/
|
||||||
async getFileSize(file: CoreWSFile, siteId?: string): Promise<number> {
|
async getFileSize(file: CoreWSFile, siteId?: string): Promise<number> {
|
||||||
try {
|
try {
|
||||||
|
@ -113,20 +99,14 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled on a site level.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Whether or not the handler is enabled on a site level.
|
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
return CoreH5P.canGetTrustedH5PFileInSite();
|
return CoreH5P.canGetTrustedH5PFileInSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file is downloadable.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param file The file data.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @returns Promise resolved with a boolean and a reason why it isn't downloadable if needed.
|
|
||||||
*/
|
*/
|
||||||
async isFileDownloadable(file: CoreWSFile, siteId?: string): Promise<CorePluginFileDownloadableResult> {
|
async isFileDownloadable(file: CoreWSFile, siteId?: string): Promise<CorePluginFileDownloadableResult> {
|
||||||
const offlineDisabled = await CoreH5P.isOfflineDisabled(siteId);
|
const offlineDisabled = await CoreH5P.isOfflineDisabled(siteId);
|
||||||
|
@ -144,31 +124,21 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the file should be treated by this handler. It is used in functions where the component isn't used.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param file The file data.
|
|
||||||
* @returns Whether the file should be treated by this handler.
|
|
||||||
*/
|
*/
|
||||||
shouldHandleFile(file: CoreWSFile): boolean {
|
shouldHandleFile(file: CoreWSFile): boolean {
|
||||||
return CoreMimetypeUtils.guessExtensionFromUrl(CoreFileHelper.getFileUrl(file)) == 'h5p';
|
return CoreMimetypeUtils.guessExtensionFromUrl(CoreFileHelper.getFileUrl(file)) == 'h5p';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Treat a downloaded file.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param fileUrl The file URL used to download the file.
|
|
||||||
* @param file The file entry of the downloaded file.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @param onProgress Function to call on progress.
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
treatDownloadedFile(
|
treatDownloadedFile(
|
||||||
fileUrl: string,
|
fileUrl: string,
|
||||||
file: FileEntry,
|
file: FileEntry,
|
||||||
siteId?: string,
|
options: CorePluginFileTreatDownloadedFileOptions = {},
|
||||||
onProgress?: CoreFilepoolOnProgressCallback,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return CoreH5PHelper.saveH5P(fileUrl, file, siteId, onProgress);
|
return CoreH5PHelper.saveH5P(fileUrl, file, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
"confirmopeninbrowser": "Do you want to open it in a web browser?",
|
"confirmopeninbrowser": "Do you want to open it in a web browser?",
|
||||||
"confirmremoveselectedfile": "This will permanently delete '{{filename}}'. You can't undo this.",
|
"confirmremoveselectedfile": "This will permanently delete '{{filename}}'. You can't undo this.",
|
||||||
"confirmremoveselectedfiles": "This will permanently delete selected files. You can't undo this.",
|
"confirmremoveselectedfiles": "This will permanently delete selected files. You can't undo this.",
|
||||||
|
"connectandtryagain": "Please connect to the internet and try again.",
|
||||||
"connectionlost": "Connection to site lost",
|
"connectionlost": "Connection to site lost",
|
||||||
"considereddigitalminor": "You are too young to create an account on this site.",
|
"considereddigitalminor": "You are too young to create an account on this site.",
|
||||||
"contactsupport": "Contact support",
|
"contactsupport": "Contact support",
|
||||||
|
@ -212,6 +213,7 @@
|
||||||
"mygroups": "My groups",
|
"mygroups": "My groups",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"needhelp": "Need help?",
|
"needhelp": "Need help?",
|
||||||
|
"needinternettoaccessit": "You need to be connected to the internet to access it.",
|
||||||
"networkerroriframemsg": "This content is not available offline. Please connect to the internet and try again.",
|
"networkerroriframemsg": "This content is not available offline. Please connect to the internet and try again.",
|
||||||
"networkerrormsg": "There was a problem connecting to the site. Please check your connection and try again.",
|
"networkerrormsg": "There was a problem connecting to the site. Please check your connection and try again.",
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
|
|
|
@ -748,19 +748,13 @@ export class CoreFilepoolProvider {
|
||||||
*
|
*
|
||||||
* @param siteId The site ID.
|
* @param siteId The site ID.
|
||||||
* @param fileUrl The file URL.
|
* @param fileUrl The file URL.
|
||||||
* @param options Extra options (revision, timemodified, isexternalfile, repositorytype).
|
* @param options Extra options.
|
||||||
* @param filePath Filepath to download the file to. If defined, no extension will be added.
|
|
||||||
* @param onProgress Function to call on progress.
|
|
||||||
* @param poolFileObject When set, the object will be updated, a new entry will not be created.
|
|
||||||
* @returns Resolved with internal URL on success, rejected otherwise.
|
* @returns Resolved with internal URL on success, rejected otherwise.
|
||||||
*/
|
*/
|
||||||
protected async downloadForPoolByUrl(
|
protected async downloadForPoolByUrl(
|
||||||
siteId: string,
|
siteId: string,
|
||||||
fileUrl: string,
|
fileUrl: string,
|
||||||
options: CoreFilepoolFileOptions = {},
|
options: DownloadForPoolOptions = {},
|
||||||
filePath?: string,
|
|
||||||
onProgress?: CoreFilepoolOnProgressCallback,
|
|
||||||
poolFileObject?: CoreFilepoolFileEntry,
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const fileId = this.getFileIdByUrl(fileUrl);
|
const fileId = this.getFileIdByUrl(fileUrl);
|
||||||
|
|
||||||
|
@ -771,10 +765,10 @@ export class CoreFilepoolProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
const extension = CoreMimetypeUtils.guessExtensionFromUrl(fileUrl);
|
const extension = CoreMimetypeUtils.guessExtensionFromUrl(fileUrl);
|
||||||
const addExtension = filePath === undefined;
|
const addExtension = options.filePath === undefined;
|
||||||
const path = filePath || (await this.getFilePath(siteId, fileId, extension, fileUrl));
|
const path = options.filePath || (await this.getFilePath(siteId, fileId, extension, fileUrl));
|
||||||
|
|
||||||
if (poolFileObject && poolFileObject.fileId !== fileId) {
|
if (options.poolFileObject && options.poolFileObject.fileId !== fileId) {
|
||||||
this.logger.error('Invalid object to update passed');
|
this.logger.error('Invalid object to update passed');
|
||||||
|
|
||||||
throw new CoreError('Invalid object to update passed.');
|
throw new CoreError('Invalid object to update passed.');
|
||||||
|
@ -794,9 +788,15 @@ export class CoreFilepoolProvider {
|
||||||
throw new CoreError(Translate.instant('core.cannotdownloadfiles'));
|
throw new CoreError(Translate.instant('core.cannotdownloadfiles'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = await CoreWS.downloadFile(fileUrl, path, addExtension, onProgress);
|
const entry = await CoreWS.downloadFile(fileUrl, path, addExtension, options.onProgress);
|
||||||
const fileEntry = entry;
|
const fileEntry = entry;
|
||||||
await CorePluginFileDelegate.treatDownloadedFile(fileUrl, fileEntry, siteId, onProgress);
|
await CorePluginFileDelegate.treatDownloadedFile(fileUrl, fileEntry, {
|
||||||
|
siteId,
|
||||||
|
onProgress: options.onProgress,
|
||||||
|
component: options.component,
|
||||||
|
componentId: options.componentId,
|
||||||
|
timemodified: options.timemodified,
|
||||||
|
});
|
||||||
|
|
||||||
await this.addFileToPool(siteId, fileId, {
|
await this.addFileToPool(siteId, fileId, {
|
||||||
downloadTime: Date.now(),
|
downloadTime: Date.now(),
|
||||||
|
@ -1126,7 +1126,14 @@ export class CoreFilepoolProvider {
|
||||||
alreadyDownloaded = false;
|
alreadyDownloaded = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress);
|
const url = await this.downloadForPoolByUrl(siteId, fileUrl, {
|
||||||
|
...options,
|
||||||
|
filePath,
|
||||||
|
onProgress,
|
||||||
|
component,
|
||||||
|
componentId,
|
||||||
|
timemodified: options.timemodified,
|
||||||
|
});
|
||||||
|
|
||||||
return finishSuccessfulDownload(url);
|
return finishSuccessfulDownload(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1220,7 +1227,7 @@ export class CoreFilepoolProvider {
|
||||||
* @param timemodified The timemodified of the file.
|
* @param timemodified The timemodified of the file.
|
||||||
* @returns Promise resolved with the file data to use.
|
* @returns Promise resolved with the file data to use.
|
||||||
*/
|
*/
|
||||||
protected async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise<CoreWSFile> {
|
async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise<CoreWSFile> {
|
||||||
const file = await CorePluginFileDelegate.getDownloadableFile({ fileurl: fileUrl, timemodified });
|
const file = await CorePluginFileDelegate.getDownloadableFile({ fileurl: fileUrl, timemodified });
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
@ -2713,7 +2720,15 @@ export class CoreFilepoolProvider {
|
||||||
const onProgress = this.getQueueOnProgress(siteId, fileId);
|
const onProgress = this.getQueueOnProgress(siteId, fileId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry);
|
await this.downloadForPoolByUrl(siteId, fileUrl, {
|
||||||
|
...options,
|
||||||
|
filePath,
|
||||||
|
onProgress,
|
||||||
|
poolFileObject: entry,
|
||||||
|
component: item.linksUnserialized?.[0]?.component,
|
||||||
|
componentId: item.linksUnserialized?.[0]?.componentId,
|
||||||
|
timemodified: options.timemodified,
|
||||||
|
});
|
||||||
|
|
||||||
// Success, we add links and remove from queue.
|
// Success, we add links and remove from queue.
|
||||||
CorePromiseUtils.ignoreErrors(this.addFileLinks(siteId, fileId, links));
|
CorePromiseUtils.ignoreErrors(this.addFileLinks(siteId, fileId, links));
|
||||||
|
@ -2752,12 +2767,12 @@ export class CoreFilepoolProvider {
|
||||||
dropFromQueue = true;
|
dropFromQueue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorMessage: string | undefined;
|
let error = errorObject;
|
||||||
// Some Android devices restrict the amount of usable storage using quotas.
|
// Some Android devices restrict the amount of usable storage using quotas.
|
||||||
// If this quota would be exceeded by the download, it throws an exception.
|
// If this quota would be exceeded by the download, it throws an exception.
|
||||||
// We catch this exception here, and report a meaningful error message to the user.
|
// We catch this exception here, and report a meaningful error message to the user.
|
||||||
if (errorObject instanceof FileTransferError && errorObject.exception && errorObject.exception.includes('EDQUOT')) {
|
if (errorObject instanceof FileTransferError && errorObject.exception && errorObject.exception.includes('EDQUOT')) {
|
||||||
errorMessage = 'core.course.insufficientavailablequota';
|
error = new Error(Translate.instant('core.course.insufficientavailablequota'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropFromQueue) {
|
if (dropFromQueue) {
|
||||||
|
@ -2765,11 +2780,11 @@ export class CoreFilepoolProvider {
|
||||||
|
|
||||||
await CorePromiseUtils.ignoreErrors(this.removeFromQueue(siteId, fileId));
|
await CorePromiseUtils.ignoreErrors(this.removeFromQueue(siteId, fileId));
|
||||||
|
|
||||||
this.treatQueueDeferred(siteId, fileId, false, errorMessage);
|
this.treatQueueDeferred(siteId, fileId, false, error);
|
||||||
this.notifyFileDownloadError(siteId, fileId, links);
|
this.notifyFileDownloadError(siteId, fileId, links);
|
||||||
} else {
|
} else {
|
||||||
// We considered the file as legit but did not get it, failure.
|
// We considered the file as legit but did not get it, failure.
|
||||||
this.treatQueueDeferred(siteId, fileId, false, errorMessage);
|
this.treatQueueDeferred(siteId, fileId, false, error);
|
||||||
this.notifyFileDownloadError(siteId, fileId, links);
|
this.notifyFileDownloadError(siteId, fileId, links);
|
||||||
|
|
||||||
throw errorObject;
|
throw errorObject;
|
||||||
|
@ -3124,12 +3139,12 @@ export class CoreFilepoolProvider {
|
||||||
* @param resolve True if promise should be resolved, false if it should be rejected.
|
* @param resolve True if promise should be resolved, false if it should be rejected.
|
||||||
* @param error String identifier for error message, if rejected.
|
* @param error String identifier for error message, if rejected.
|
||||||
*/
|
*/
|
||||||
protected treatQueueDeferred(siteId: string, fileId: string, resolve: boolean, error?: string): void {
|
protected treatQueueDeferred(siteId: string, fileId: string, resolve: boolean, error?: Error | string): void {
|
||||||
if (siteId in this.queueDeferreds && fileId in this.queueDeferreds[siteId]) {
|
if (siteId in this.queueDeferreds && fileId in this.queueDeferreds[siteId]) {
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
this.queueDeferreds[siteId][fileId].resolve();
|
this.queueDeferreds[siteId][fileId].resolve();
|
||||||
} else {
|
} else {
|
||||||
this.queueDeferreds[siteId][fileId].reject(new Error(error));
|
this.queueDeferreds[siteId][fileId].reject(typeof error === 'string' ? new Error(error) : error);
|
||||||
}
|
}
|
||||||
delete this.queueDeferreds[siteId][fileId];
|
delete this.queueDeferreds[siteId][fileId];
|
||||||
}
|
}
|
||||||
|
@ -3241,3 +3256,15 @@ type CoreFilepoolPromisedValue = CorePromisedValue<void> & {
|
||||||
|
|
||||||
type AnchorOrMediaElement =
|
type AnchorOrMediaElement =
|
||||||
HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement;
|
HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for downloadForPoolByUrl.
|
||||||
|
*/
|
||||||
|
type DownloadForPoolOptions = CoreFilepoolFileOptions & {
|
||||||
|
filePath?: string; // Filepath to download the file to. If defined, no extension will be added.
|
||||||
|
onProgress?: CoreFilepoolOnProgressCallback; // Function to call on progress.
|
||||||
|
poolFileObject?: CoreFilepoolFileEntry; // When set, the object will be updated, a new entry will not be created.
|
||||||
|
component?: string; // The component to link the file to.
|
||||||
|
componentId?: string | number; // An ID to use in conjunction with the component.
|
||||||
|
timemodified?: number; // The time the file was modified.
|
||||||
|
};
|
||||||
|
|
|
@ -271,20 +271,18 @@ export class CorePluginFileDelegateService extends CoreDelegate<CorePluginFileHa
|
||||||
*
|
*
|
||||||
* @param fileUrl The file URL used to download the file.
|
* @param fileUrl The file URL used to download the file.
|
||||||
* @param file The file entry of the downloaded file.
|
* @param file The file entry of the downloaded file.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param options Options.
|
||||||
* @param onProgress Function to call on progress.
|
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async treatDownloadedFile(
|
async treatDownloadedFile(
|
||||||
fileUrl: string,
|
fileUrl: string,
|
||||||
file: FileEntry,
|
file: FileEntry,
|
||||||
siteId?: string,
|
options: CorePluginFileTreatDownloadedFileOptions = {},
|
||||||
onProgress?: CoreFilepoolOnProgressCallback,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const handler = this.getHandlerForFile({ fileurl: fileUrl });
|
const handler = this.getHandlerForFile({ fileurl: fileUrl });
|
||||||
|
|
||||||
if (handler && handler.treatDownloadedFile) {
|
if (handler && handler.treatDownloadedFile) {
|
||||||
await handler.treatDownloadedFile(fileUrl, file, siteId, onProgress);
|
await handler.treatDownloadedFile(fileUrl, file, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,16 +376,14 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
|
||||||
*
|
*
|
||||||
* @param fileUrl The file URL used to download the file.
|
* @param fileUrl The file URL used to download the file.
|
||||||
* @param file The file entry of the downloaded file.
|
* @param file The file entry of the downloaded file.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param options Options.
|
||||||
* @param onProgress Function to call on progress.
|
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
treatDownloadedFile?(
|
treatDownloadedFile?(
|
||||||
fileUrl: string,
|
fileUrl: string,
|
||||||
file: FileEntry,
|
file: FileEntry,
|
||||||
siteId?: string,
|
options?: CorePluginFileTreatDownloadedFileOptions,
|
||||||
onProgress?: CoreFilepoolOnProgressCallback):
|
): Promise<void>;
|
||||||
Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -412,3 +408,14 @@ export type CoreFileSizeSum = {
|
||||||
size: number; // Sum of file sizes.
|
size: number; // Sum of file sizes.
|
||||||
total: boolean; // False if any file size is not available.
|
total: boolean; // False if any file size is not available.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for treatDownloadedFile.
|
||||||
|
*/
|
||||||
|
export type CorePluginFileTreatDownloadedFileOptions<T = unknown> = {
|
||||||
|
siteId?: string; // Site ID. If not defined, current site.
|
||||||
|
onProgress?: CoreFilepoolOnProgressCallback<T>; // Function to call on progress.
|
||||||
|
component?: string; // The component to link the file to.
|
||||||
|
componentId?: string | number; // An ID to use in conjunction with the component.
|
||||||
|
timemodified?: number; // The timemodified of the file.
|
||||||
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ For more information about upgrading, read the official documentation: https://m
|
||||||
=== 5.0.0 ===
|
=== 5.0.0 ===
|
||||||
|
|
||||||
- The logout process has been refactored, now it uses a logout page to trigger Angular guards. CoreSites.logout now uses this process, and CoreSites.logoutForRedirect is deprecated and shouldn't be used anymore.
|
- The logout process has been refactored, now it uses a logout page to trigger Angular guards. CoreSites.logout now uses this process, and CoreSites.logoutForRedirect is deprecated and shouldn't be used anymore.
|
||||||
|
- The parameters of treatDownloadedFile of plugin file handlers have changed. Now the third parameter is an object with all the optional parameters.
|
||||||
|
|
||||||
=== 4.5.0 ===
|
=== 4.5.0 ===
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue