MOBILE-4669 scorm: Fix height problems in online player

main
Dani Palou 2024-11-28 09:54:34 +01:00
parent bab16335d0
commit 20a39bbcb5
5 changed files with 126 additions and 44 deletions

View File

@ -980,10 +980,7 @@
"addon.mod_scorm.errorcreateofflineattempt": "local_moodlemobileapp", "addon.mod_scorm.errorcreateofflineattempt": "local_moodlemobileapp",
"addon.mod_scorm.errordownloadscorm": "local_moodlemobileapp", "addon.mod_scorm.errordownloadscorm": "local_moodlemobileapp",
"addon.mod_scorm.errorgetscorm": "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.errornovalidsco": "local_moodlemobileapp",
"addon.mod_scorm.errorpackagefile": "local_moodlemobileapp",
"addon.mod_scorm.errorsyncscorm": "local_moodlemobileapp", "addon.mod_scorm.errorsyncscorm": "local_moodlemobileapp",
"addon.mod_scorm.exceededmaxattempts": "scorm", "addon.mod_scorm.exceededmaxattempts": "scorm",
"addon.mod_scorm.failed": "scorm", "addon.mod_scorm.failed": "scorm",

View File

@ -16,7 +16,7 @@
<ion-content> <ion-content>
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<core-iframe *ngIf="loaded" id="scorm_object" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight" <core-iframe *ngIf="loaded" id="scorm_object" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"
[showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true" /> [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="enableFullScreenOnRotate" (loaded)="iframeLoaded()" />
<p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p> <p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
</core-loading> </core-loading>

View File

@ -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 { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
@ -31,6 +31,8 @@ import { Subscription } from 'rxjs';
import { CoreNetwork } from '@services/network'; import { CoreNetwork } from '@services/network';
import { NgZone, Translate } from '@singletons'; import { NgZone, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error'; 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. * 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 { export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy {
@ViewChild(CoreIframeComponent) iframe?: CoreIframeComponent;
scorm!: AddonModScormScorm; // The SCORM object. scorm!: AddonModScormScorm; // The SCORM object.
loaded = false; // Whether the data has been loaded. loaded = false; // Whether the data has been loaded.
src?: string; // Iframe src. src?: string; // Iframe src.
@ -53,6 +57,7 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy
scormHeight?: number; // Height applied to scorm iframe. scormHeight?: number; // Height applied to scorm iframe.
cmId!: number; // Course module ID. cmId!: number; // Course module ID.
courseId!: number; // Course ID. courseId!: number; // Course ID.
enableFullScreenOnRotate = false;
protected mode!: AddonModScormMode; // Mode to play the SCORM. protected mode!: AddonModScormMode; // Mode to play the SCORM.
protected moduleUrl!: string; // Module URL. protected moduleUrl!: string; // Module URL.
@ -73,9 +78,9 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy
if (!isOnline && wasOnline) { if (!isOnline && wasOnline) {
// User lost connection while playing an online package. Show an error. // User lost connection while playing an online package. Show an error.
CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'))); // , { CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'), {
// title: Translate.instant('core.youreoffline'), title: Translate.instant('core.youreoffline'),
// })); }));
return; return;
} }
@ -114,7 +119,6 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy
} finally { } finally {
this.loaded = true; this.loaded = true;
} }
} }
/** /**
@ -275,4 +279,21 @@ export default class AddonModScormOnlinePlayerPage implements OnInit, OnDestroy
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'scorm' }); CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'scorm' });
} }
/**
* SCORM iframe has been loaded.
*/
async iframeLoaded(): Promise<void> {
// 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;
}
} }

View File

@ -159,13 +159,27 @@ Feature: Test basic usage of SCORM activity in app
# And I go back in the 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 # 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: Given the following "activities" exist:
| activity | name | course | idnumber | packagefilepath | | activity | name | course | idnumber | packagefilepath |
| scorm | SCORM 1.2 | C1 | scorm | mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip | | scorm | SCORM 2004 | C1 | scorm | mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip |
And I entered the course "Course 1" as "student1" in the app And I entered the course "Course 1" as "student1" in the app
When I press "SCORM 1.2" in the app When I press "SCORM 2004" in the app
Then I should find "Sorry, the application only supports SCORM 1.2." 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 Scenario: Hidden SCOs not displayed in TOC
Given the following "activities" exist: Given the following "activities" exist:

View File

@ -64,6 +64,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
initialized = false; initialized = false;
protected fullScreenInitialized = false;
protected iframe?: HTMLIFrameElement; protected iframe?: HTMLIFrameElement;
protected style?: HTMLStyleElement; protected style?: HTMLStyleElement;
protected orientationObs?: CoreEventObserver; protected orientationObs?: CoreEventObserver;
@ -87,36 +88,6 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
this.initialized = true; 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. // Show loading only with external URLs.
this.loading = !this.src || !CoreUrl.isLocalFileUrl(this.src); 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. * Initialize things related to the iframe element.
*/ */
@ -170,6 +207,10 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
} }
if (!changes.src) { if (!changes.src) {
if (changes.showFullscreenOnToolbar || changes.autoFullscreenOnRotate) {
this.configureFullScreen();
}
return; 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. // Now that the URL has been set, initialize the iframe. Wait for the iframe to the added to the DOM.
setTimeout(() => { setTimeout(() => {
this.init(); this.init();
if (changes.showFullscreenOnToolbar || changes.autoFullscreenOnRotate) {
this.configureFullScreen();
}
}); });
} }
@ -229,6 +273,12 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
this.orientationObs?.off(); this.orientationObs?.off();
this.navSubscription?.unsubscribe(); this.navSubscription?.unsubscribe();
window.removeEventListener('message', this.messageListenerFunction); 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);
}
} }
/** /**