From 20a39bbcb5bee14018cde9361edbdfc30d98b5ae Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 28 Nov 2024 09:54:34 +0100 Subject: [PATCH] MOBILE-4669 scorm: Fix height problems in online player --- scripts/langindex.json | 3 - .../pages/online-player/online-player.html | 2 +- .../pages/online-player/online-player.ts | 31 ++++- .../mod/scorm/tests/behat/basic_usage.feature | 24 +++- src/core/components/iframe/iframe.ts | 110 +++++++++++++----- 5 files changed, 126 insertions(+), 44 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 480df6cfd..3f8d345ee 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -980,10 +980,7 @@ "addon.mod_scorm.errorcreateofflineattempt": "local_moodlemobileapp", "addon.mod_scorm.errordownloadscorm": "local_moodlemobileapp", "addon.mod_scorm.errorgetscorm": "local_moodlemobileapp", - "addon.mod_scorm.errorinvalidversion": "local_moodlemobileapp", - "addon.mod_scorm.errornotdownloadable": "local_moodlemobileapp", "addon.mod_scorm.errornovalidsco": "local_moodlemobileapp", - "addon.mod_scorm.errorpackagefile": "local_moodlemobileapp", "addon.mod_scorm.errorsyncscorm": "local_moodlemobileapp", "addon.mod_scorm.exceededmaxattempts": "scorm", "addon.mod_scorm.failed": "scorm", diff --git a/src/addons/mod/scorm/pages/online-player/online-player.html b/src/addons/mod/scorm/pages/online-player/online-player.html index 97d6682b1..3dc34a1f3 100644 --- a/src/addons/mod/scorm/pages/online-player/online-player.html +++ b/src/addons/mod/scorm/pages/online-player/online-player.html @@ -16,7 +16,7 @@ + [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="enableFullScreenOnRotate" (loaded)="iframeLoaded()" />

{{ errorMessage | translate }}

diff --git a/src/addons/mod/scorm/pages/online-player/online-player.ts b/src/addons/mod/scorm/pages/online-player/online-player.ts index e4c86d574..41e2115db 100644 --- a/src/addons/mod/scorm/pages/online-player/online-player.ts +++ b/src/addons/mod/scorm/pages/online-player/online-player.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { CoreNavigator } from '@services/navigator'; import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -31,6 +31,8 @@ import { Subscription } from 'rxjs'; import { CoreNetwork } from '@services/network'; import { NgZone, Translate } from '@singletons'; import { CoreError } from '@classes/errors/error'; +import { CoreWait } from '@singletons/wait'; +import { CoreIframeComponent } from '@components/iframe/iframe'; /** * Page that allows playing a SCORM in online, served from the server. @@ -45,6 +47,8 @@ import { CoreError } from '@classes/errors/error'; }) export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy { + @ViewChild(CoreIframeComponent) iframe?: CoreIframeComponent; + scorm!: AddonModScormScorm; // The SCORM object. loaded = false; // Whether the data has been loaded. src?: string; // Iframe src. @@ -53,6 +57,7 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy scormHeight?: number; // Height applied to scorm iframe. cmId!: number; // Course module ID. courseId!: number; // Course ID. + enableFullScreenOnRotate = false; protected mode!: AddonModScormMode; // Mode to play the SCORM. protected moduleUrl!: string; // Module URL. @@ -73,9 +78,9 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy if (!isOnline && wasOnline) { // User lost connection while playing an online package. Show an error. - CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'))); // , { - // title: Translate.instant('core.youreoffline'), - // })); + CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'), { + title: Translate.instant('core.youreoffline'), + })); return; } @@ -114,7 +119,6 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy } finally { this.loaded = true; } - } /** @@ -275,4 +279,21 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'scorm' }); } + /** + * SCORM iframe has been loaded. + */ + async iframeLoaded(): Promise { + // When using online player, some packages don't calculate the right height. Sending a 'resize' event doesn't fix it, but + // changing the iframe size makes the SCORM recalculate the size. + // Wait 1 second (to let inner iframes load) and then force full screen to make the SCORM recalculate the size. + await CoreWait.wait(1000); + + if (this.isDestroyed) { + return; + } + + this.iframe?.toggleFullscreen(true); + this.enableFullScreenOnRotate = true; + } + } diff --git a/src/addons/mod/scorm/tests/behat/basic_usage.feature b/src/addons/mod/scorm/tests/behat/basic_usage.feature index df8bd4c4f..72eedf85e 100755 --- a/src/addons/mod/scorm/tests/behat/basic_usage.feature +++ b/src/addons/mod/scorm/tests/behat/basic_usage.feature @@ -159,13 +159,27 @@ Feature: Test basic usage of SCORM activity in app # And I go back in the app # Then I should find "1" within "Number of attempts you have made" "ion-item" in the app - Scenario: Unsupported SCORM + Scenario: SCORM 2004 works online Given the following "activities" exist: - | activity | name | course | idnumber | packagefilepath | - | scorm | SCORM 1.2 | C1 | scorm | mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip | + | activity | name | course | idnumber | packagefilepath | + | scorm | SCORM 2004 | C1 | scorm | mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip | And I entered the course "Course 1" as "student1" in the app - When I press "SCORM 1.2" in the app - Then I should find "Sorry, the application only supports SCORM 1.2." in the app + When I press "SCORM 2004" in the app + Then I should be able to press "Enter" in the app + + When I switch network connection to offline + Then I should find "This activity is not available offline" in the app + And I should not be able to press "Enter" in the app + + When I switch network connection to wifi + And I press "Enter" in the app + And I press "Disable fullscreen" in the app + Then I should not be able to press "TOC" in the app + + When I switch network connection to offline + Then I should find "Any changes you make to this activity while offline may not be saved" in the app + + # TODO: When iframes are fixed, test that the iframe actually works. However, the Golf 2004 SCORM has some issues. Scenario: Hidden SCOs not displayed in TOC Given the following "activities" exist: diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts index 761781cb8..db236dba2 100644 --- a/src/core/components/iframe/iframe.ts +++ b/src/core/components/iframe/iframe.ts @@ -64,6 +64,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { initialized = false; + protected fullScreenInitialized = false; protected iframe?: HTMLIFrameElement; protected style?: HTMLStyleElement; protected orientationObs?: CoreEventObserver; @@ -87,36 +88,6 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { this.initialized = true; - if (this.showFullscreenOnToolbar || this.autoFullscreenOnRotate) { - // Leave fullscreen when navigating. - this.navSubscription = Router.events - .pipe(filter(event => event instanceof NavigationStart)) - .subscribe(async () => { - if (this.fullscreen) { - this.toggleFullscreen(false); - } - }); - - const shadow = - this.elementRef.nativeElement.closest('.ion-page')?.querySelector('ion-header ion-toolbar')?.shadowRoot; - if (shadow) { - this.style = document.createElement('style'); - shadow.appendChild(this.style); - } - - if (this.autoFullscreenOnRotate) { - this.toggleFullscreen(CoreScreen.isLandscape); - - this.orientationObs = CoreEvents.on(CoreEvents.ORIENTATION_CHANGE, (data) => { - if (this.isInHiddenPage()) { - return; - } - - this.toggleFullscreen(data.orientation == CoreScreenOrientation.LANDSCAPE); - }); - } - } - // Show loading only with external URLs. this.loading = !this.src || !CoreUrl.isLocalFileUrl(this.src); @@ -127,6 +98,72 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { } } + /** + * Configure fullscreen based on the inputs. + */ + protected configureFullScreen(): void { + if (!this.showFullscreenOnToolbar && !this.autoFullscreenOnRotate) { + // Full screen disabled, stop watchers if enabled. + this.navSubscription?.unsubscribe(); + this.orientationObs?.off(); + this.style?.remove(); + this.navSubscription = undefined; + this.orientationObs = undefined; + this.style = undefined; + this.fullScreenInitialized = true; + + return; + } + + if (!this.navSubscription) { + // Leave fullscreen when navigating. + this.navSubscription = Router.events + .pipe(filter(event => event instanceof NavigationStart)) + .subscribe(async () => { + if (this.fullscreen) { + this.toggleFullscreen(false); + } + }); + } + + if (!this.style) { + const shadow = this.elementRef.nativeElement.closest('.ion-page')?.querySelector('ion-header ion-toolbar')?.shadowRoot; + if (shadow) { + this.style = document.createElement('style'); + shadow.appendChild(this.style); + } + } + + if (!this.autoFullscreenOnRotate) { + this.orientationObs?.off(); + this.orientationObs = undefined; + this.fullScreenInitialized = true; + + return; + } + + if (this.orientationObs) { + this.fullScreenInitialized = true; + + return; + } + + if (!this.fullScreenInitialized) { + // Only change full screen value if it's being initialized. + this.toggleFullscreen(CoreScreen.isLandscape); + } + + this.orientationObs = CoreEvents.on(CoreEvents.ORIENTATION_CHANGE, (data) => { + if (this.isInHiddenPage()) { + return; + } + + this.toggleFullscreen(data.orientation == CoreScreenOrientation.LANDSCAPE); + }); + + this.fullScreenInitialized = true; + } + /** * Initialize things related to the iframe element. */ @@ -170,6 +207,10 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { } if (!changes.src) { + if (changes.showFullscreenOnToolbar || changes.autoFullscreenOnRotate) { + this.configureFullScreen(); + } + return; } @@ -212,6 +253,9 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { // Now that the URL has been set, initialize the iframe. Wait for the iframe to the added to the DOM. setTimeout(() => { this.init(); + if (changes.showFullscreenOnToolbar || changes.autoFullscreenOnRotate) { + this.configureFullScreen(); + } }); } @@ -229,6 +273,12 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { this.orientationObs?.off(); this.navSubscription?.unsubscribe(); window.removeEventListener('message', this.messageListenerFunction); + + if (this.fullscreen) { + // Make sure to leave fullscreen mode when the iframe is destroyed. This can happen if there's a race condition + // between clicking back button and some code toggling the fullscreen on. + this.toggleFullscreen(false); + } } /**