From 9b637fc496da5884155903a06a95811b297b188e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 31 Oct 2019 12:30:45 +0100 Subject: [PATCH] MOBILE-2235 h5p: Display download button in H5P placeholder --- src/components/iframe/core-iframe.html | 2 +- src/components/iframe/iframe.scss | 3 - src/core/compile/providers/compile.ts | 3 +- src/core/filter/providers/filter.ts | 2 +- .../h5p-player/core-h5p-player.html | 11 +- .../h5p/components/h5p-player/h5p-player.scss | 39 +++- .../h5p/components/h5p-player/h5p-player.ts | 80 ++++++++- src/core/h5p/h5p.module.ts | 7 + src/core/h5p/providers/h5p.ts | 169 ++++++++++++++++++ src/theme/variables.scss | 3 - 10 files changed, 300 insertions(+), 19 deletions(-) create mode 100644 src/core/h5p/providers/h5p.ts diff --git a/src/components/iframe/core-iframe.html b/src/components/iframe/core-iframe.html index a94b5d110..7bdeac425 100644 --- a/src/components/iframe/core-iframe.html +++ b/src/components/iframe/core-iframe.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/components/iframe/iframe.scss b/src/components/iframe/iframe.scss index 0d0e5ca92..782a554cb 100644 --- a/src/components/iframe/iframe.scss +++ b/src/components/iframe/iframe.scss @@ -1,7 +1,4 @@ ion-app.app-root core-iframe { - > div { - height: 100%; - } iframe { border: 0; display: block; diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index bbfc35f48..8f0c7e1b6 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -30,6 +30,7 @@ import { CORE_COURSES_PROVIDERS } from '@core/courses/courses.module'; import { CORE_FILEUPLOADER_PROVIDERS } from '@core/fileuploader/fileuploader.module'; import { CORE_FILTER_PROVIDERS } from '@core/filter/filter.module'; import { CORE_GRADES_PROVIDERS } from '@core/grades/grades.module'; +import { CORE_H5P_PROVIDERS } from '@core/h5p/h5p.module'; import { CORE_LOGIN_PROVIDERS } from '@core/login/login.module'; import { CORE_MAINMENU_PROVIDERS } from '@core/mainmenu/mainmenu.module'; import { CORE_QUESTION_PROVIDERS } from '@core/question/question.module'; @@ -236,7 +237,7 @@ export class CoreCompileProvider { .concat(ADDON_MOD_SURVEY_PROVIDERS).concat(ADDON_MOD_URL_PROVIDERS).concat(ADDON_MOD_WIKI_PROVIDERS) .concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS) .concat(CORE_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS) - .concat(CORE_FILTER_PROVIDERS); + .concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS); // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. for (const i in providers) { diff --git a/src/core/filter/providers/filter.ts b/src/core/filter/providers/filter.ts index 42802335f..a404d5616 100644 --- a/src/core/filter/providers/filter.ts +++ b/src/core/filter/providers/filter.ts @@ -402,7 +402,7 @@ export type CoreFilterFilter = { */ export type CoreFilterGetAvailableInContextResult = { filters: CoreFilterFilter[]; // Available filters. - warning: CoreWSExternalWarning[]; // List of warnings. + warnings: CoreWSExternalWarning[]; // List of warnings. }; /** diff --git a/src/core/h5p/components/h5p-player/core-h5p-player.html b/src/core/h5p/components/h5p-player/core-h5p-player.html index 3d9e67912..af48001df 100644 --- a/src/core/h5p/components/h5p-player/core-h5p-player.html +++ b/src/core/h5p/components/h5p-player/core-h5p-player.html @@ -1,9 +1,14 @@
- - + +
{{ errorMessage }}
+ +
+ +
- + diff --git a/src/core/h5p/components/h5p-player/h5p-player.scss b/src/core/h5p/components/h5p-player/h5p-player.scss index 0c963aa79..b5ba7f82c 100644 --- a/src/core/h5p/components/h5p-player/h5p-player.scss +++ b/src/core/h5p/components/h5p-player/h5p-player.scss @@ -1,23 +1,56 @@ +// H5P variables. +$core-h5p-placeholder-bg-color: $gray !default; +$core-h5p-placeholder-text-color: $text-color !default; + ion-app.app-root core-h5p-player { .core-h5p-placeholder { position: relative; width: 100%; height: 230px; background: url('../assets/img/icons/h5p.svg') center top 25px / 100px auto no-repeat $core-h5p-placeholder-bg-color; + color: $core-h5p-placeholder-text-color; - .core-h5p-placeholder-play-button { + .icon { + color: $core-h5p-placeholder-text-color; + } + + .core-h5p-placeholder-play-button, .core-h5p-placeholder-spinner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); + } + + .core-h5p-placeholder-play-button { font-size: 30px; min-height: 50px; } .core-h5p-placeholder-download-container { position: absolute; - right: 10px; - font-size: 1.8em; + top: 0; + right: 0; + + ion-spinner { + margin-right: 0.75em; + } + + core-download-refresh > ion-icon { + margin: 0.4rem 0.2rem; + padding: 0 0.5em; + line-height: .67; + } + } + + .core-h5p-placeholder-error { + position: absolute; + width: 100%; + text-align: center; + top: 50%; + } + + ion-spinner circle { + stroke: $core-h5p-placeholder-text-color; } } diff --git a/src/core/h5p/components/h5p-player/h5p-player.ts b/src/core/h5p/components/h5p-player/h5p-player.ts index e6a73b2a4..0c4f3e937 100644 --- a/src/core/h5p/components/h5p-player/h5p-player.ts +++ b/src/core/h5p/components/h5p-player/h5p-player.ts @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, ElementRef } from '@angular/core'; +import { Component, Input, ElementRef, OnInit, SimpleChange } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreH5PProvider } from '@core/h5p/providers/h5p'; /** * Component to render an H5P package. @@ -22,17 +26,39 @@ import { CoreSitesProvider } from '@providers/sites'; selector: 'core-h5p-player', templateUrl: 'core-h5p-player.html' }) -export class CoreH5PPlayerComponent { +export class CoreH5PPlayerComponent implements OnInit { @Input() src: string; // The URL of the player to display the H5P package. + playerSrc: string; showPackage = false; loading = false; status: string; canDownload: boolean; calculating = true; + errorMessage: string; constructor(public elementRef: ElementRef, - protected sitesProvider: CoreSitesProvider) { + protected sitesProvider: CoreSitesProvider, + protected urlUtils: CoreUrlUtilsProvider, + protected utils: CoreUtilsProvider, + protected textUtils: CoreTextUtilsProvider, + protected h5pProvider: CoreH5PProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.checkCanDownload(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + // If it's already playing and the src changes, don't change the player src, the user could lose data. + if (changes.src && !this.showPackage) { + this.checkCanDownload(); + } } /** @@ -46,11 +72,57 @@ export class CoreH5PPlayerComponent { this.loading = true; + // @TODO: Check if package is downloaded and use the local player if so. + // Get auto-login URL so the user is automatically authenticated. this.sitesProvider.getCurrentSite().getAutoLoginUrl(this.src, false).then((url) => { - this.src = url; + this.playerSrc = url; this.loading = false; this.showPackage = true; }); } + + /** + * Download the package. + */ + download(): void { + // @TODO: Implement package download. + } + + /** + * Check if the package can be downloaded. + */ + protected checkCanDownload(): void { + if (this.src && this.h5pProvider.canGetTrustedH5PFileInSite()) { + const params = this.urlUtils.extractUrlParams(this.src); + + // @todo: Check if H5P offline is disabled in the site. + + // Now check if the package can be played. + this.calculating = true; + + const options = { + frame: this.utils.isTrueOrOne(params.frame), + export: this.utils.isTrueOrOne(params.export), + embed: this.utils.isTrueOrOne(params.embed), + copyright: this.utils.isTrueOrOne(params.copyright), + }; + + this.h5pProvider.getTrustedH5PFile(params.url, options).then((file) => { + this.canDownload = true; + this.errorMessage = undefined; + }).catch((error) => { + this.canDownload = false; + this.errorMessage = this.textUtils.getErrorMessageFromError(error); + }).finally(() => { + this.calculating = false; + }); + + return; + } + + this.calculating = false; + this.canDownload = false; + this.errorMessage = undefined; + } } diff --git a/src/core/h5p/h5p.module.ts b/src/core/h5p/h5p.module.ts index 4fb4f6ba4..ddc625a0b 100644 --- a/src/core/h5p/h5p.module.ts +++ b/src/core/h5p/h5p.module.ts @@ -14,6 +14,12 @@ import { NgModule } from '@angular/core'; import { CoreH5PComponentsModule } from './components/components.module'; +import { CoreH5PProvider } from './providers/h5p'; + +// List of providers (without handlers). +export const CORE_H5P_PROVIDERS: any[] = [ + CoreH5PProvider +]; @NgModule({ declarations: [], @@ -21,6 +27,7 @@ import { CoreH5PComponentsModule } from './components/components.module'; CoreH5PComponentsModule ], providers: [ + CoreH5PProvider ], exports: [] }) diff --git a/src/core/h5p/providers/h5p.ts b/src/core/h5p/providers/h5p.ts new file mode 100644 index 000000000..335e5025f --- /dev/null +++ b/src/core/h5p/providers/h5p.ts @@ -0,0 +1,169 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; + +/** + * Service to provide H5P functionalities. + */ +@Injectable() +export class CoreH5PProvider { + + protected ROOT_CACHE_KEY = 'mmH5P:'; + + protected logger; + + constructor(logger: CoreLoggerProvider, + private sitesProvider: CoreSitesProvider) { + + this.logger = logger.getInstance('CoreFilterProvider'); + } + + /** + * Returns whether or not WS to get trusted H5P file is available. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if ws is available, false otherwise. + * @since 3.8 + */ + canGetTrustedH5PFile(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.canGetTrustedH5PFileInSite(site); + }); + } + + /** + * Returns whether or not WS to get trusted H5P file is available in a certain site. + * + * @param site Site. If not defined, current site. + * @return Promise resolved with true if ws is available, false otherwise. + * @since 3.4 + */ + canGetTrustedH5PFileInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('core_h5p_get_trusted_h5p_file'); + } + + /** + * Get a trusted H5P file. + * + * @param url The file URL. + * @param options Options. + * @param ignoreCache Whether to ignore cache.. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the file data. + */ + getTrustedH5PFile(url: string, options: CoreH5PGetTrustedFileOptions, ignoreCache?: boolean, siteId?: string) + : Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data = { + url: url, + frame: options.frame ? 1 : 0, + export: options.export ? 1 : 0, + embed: options.embed ? 1 : 0, + copyright: options.copyright ? 1 : 0, + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getTrustedH5PFileCacheKey(url), + updateFrequency: CoreSite.FREQUENCY_RARELY + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('core_h5p_get_trusted_h5p_file', data, preSets).then((result: CoreH5PGetTrustedH5PFileResult): any => { + if (result.warnings && result.warnings.length) { + return Promise.reject(result.warnings[0]); + } + + if (result.files && result.files.length) { + return result.files[0]; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get cache key for trusted H5P file WS calls. + * + * @param url The file URL. + * @return Cache key. + */ + protected getTrustedH5PFileCacheKey(url: string): string { + return this.getTrustedH5PFilePrefixCacheKey() + url; + } + + /** + * Get prefixed cache key for trusted H5P file WS calls. + * + * @return Cache key. + */ + protected getTrustedH5PFilePrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'trustedH5PFile:'; + } + + /** + * Invalidates all trusted H5P file WS calls. + * + * @param siteId Site ID (empty for current site). + * @return Promise resolved when the data is invalidated. + */ + invalidateAllGetTrustedH5PFile(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getTrustedH5PFilePrefixCacheKey()); + }); + } + + /** + * Invalidates get trusted H5P file WS call. + * + * @param url The URL of the file. + * @param siteId Site ID (empty for current site). + * @return Promise resolved when the data is invalidated. + */ + invalidateAvailableInContexts(url: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url)); + }); + } +} + +/** + * Options for core_h5p_get_trusted_h5p_file. + */ +export type CoreH5PGetTrustedFileOptions = { + frame?: boolean; // Whether to show the bar options below the content. + export?: boolean; // Whether to allow to download the package. + embed?: boolean; // Whether to allow to copy the code to your site. + copyright?: boolean; // The copyright option. +}; + +/** + * Result of core_h5p_get_trusted_h5p_file. + */ +export type CoreH5PGetTrustedH5PFileResult = { + files: CoreWSExternalFile[]; // Files. + warnings: CoreWSExternalWarning[]; // List of warnings. +}; diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 326cc4671..ebab93935 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -369,9 +369,6 @@ $core-question-state-incorrect-color: $red-light !default; $core-dd-question-selected-shadow: 2px 2px 4px $gray-dark !default; $core-dd-question-colors: $white, $blue-light, #DCDCDC, #D8BFD8, #87CEFA, #DAA520, #FFD700, #F0E68C !default; -// H5P variables. -$core-h5p-placeholder-bg-color: $gray-dark !default; - // Mixins // ------------------------- @mixin core-transition($where: all, $time: 500ms) {