forked from CIT/Vmeda.Online
		
	
						commit
						4cf3d3d80d
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -28,6 +28,7 @@ npm-debug.log*
 | 
			
		||||
/platforms
 | 
			
		||||
/plugins
 | 
			
		||||
/www
 | 
			
		||||
/src/assets/lib
 | 
			
		||||
 | 
			
		||||
/moodle.*.config.json
 | 
			
		||||
!/moodle.example.config.json
 | 
			
		||||
 | 
			
		||||
@ -220,6 +220,9 @@
 | 
			
		||||
                <param name="android-package" value="org.apache.cordova.media.AudioHandler" />
 | 
			
		||||
            </feature>
 | 
			
		||||
        </config-file>
 | 
			
		||||
        <config-file parent="/*" target="AndroidManifest.xml">
 | 
			
		||||
            <uses-feature android:name="android.hardware.bluetooth" android:required="false" />
 | 
			
		||||
        </config-file>
 | 
			
		||||
    </platform>
 | 
			
		||||
    <platform name="ios">
 | 
			
		||||
        <resource-file src="GoogleService-Info.plist" />
 | 
			
		||||
 | 
			
		||||
@ -3,5 +3,9 @@
 | 
			
		||||
  "integrations": {
 | 
			
		||||
    "cordova": {}
 | 
			
		||||
  },
 | 
			
		||||
  "type": "angular"
 | 
			
		||||
}
 | 
			
		||||
  "type": "angular",
 | 
			
		||||
  "hooks": {
 | 
			
		||||
    "build:before": "./scripts/copy-assets.js",
 | 
			
		||||
    "serve:before": "./scripts/copy-assets.js"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
    "app_id": "com.moodle.moodlemobile",
 | 
			
		||||
    "appname": "Moodle Mobile",
 | 
			
		||||
    "versioncode": 3930,
 | 
			
		||||
    "versionname": "3.9.3-dev",
 | 
			
		||||
    "versioncode": 3950,
 | 
			
		||||
    "versionname": "3.9.5-dev",
 | 
			
		||||
    "cache_update_frequency_usually": 420000,
 | 
			
		||||
    "cache_update_frequency_often": 1200000,
 | 
			
		||||
    "cache_update_frequency_sometimes": 3600000,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -10531,14 +10531,14 @@
 | 
			
		||||
      "optional": true
 | 
			
		||||
    },
 | 
			
		||||
    "fs-extra": {
 | 
			
		||||
      "version": "9.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
 | 
			
		||||
      "version": "9.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "at-least-node": "^1.0.0",
 | 
			
		||||
        "graceful-fs": "^4.2.0",
 | 
			
		||||
        "jsonfile": "^6.0.1",
 | 
			
		||||
        "universalify": "^1.0.0"
 | 
			
		||||
        "universalify": "^2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "fs-minipass": {
 | 
			
		||||
@ -14192,12 +14192,12 @@
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "jsonfile": {
 | 
			
		||||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
 | 
			
		||||
      "version": "6.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "graceful-fs": "^4.1.6",
 | 
			
		||||
        "universalify": "^1.0.0"
 | 
			
		||||
        "universalify": "^2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "jsonparse": {
 | 
			
		||||
@ -15061,6 +15061,11 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "mathjax": {
 | 
			
		||||
      "version": "2.7.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.7.tgz",
 | 
			
		||||
      "integrity": "sha512-OOl0B2/0tSJAtAZarXnQuLDBLgTNRqiI9VqHTQzPsxf4okT2iIpDrvaklK9x2QEMD1sDj4yRn11Ygci41DxMAQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "md5-file": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz",
 | 
			
		||||
@ -21796,9 +21801,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "universalify": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "unorm": {
 | 
			
		||||
      "version": "1.6.0",
 | 
			
		||||
 | 
			
		||||
@ -110,6 +110,7 @@
 | 
			
		||||
    "core-js": "^3.9.1",
 | 
			
		||||
    "es6-promise-plugin": "^4.2.2",
 | 
			
		||||
    "jszip": "^3.5.0",
 | 
			
		||||
    "mathjax": "2.7.7",
 | 
			
		||||
    "moment": "^2.29.0",
 | 
			
		||||
    "nl.kingsquare.cordova.background-audio": "^1.0.1",
 | 
			
		||||
    "phonegap-plugin-multidex": "^1.0.0",
 | 
			
		||||
@ -149,6 +150,7 @@
 | 
			
		||||
    "eslint-plugin-prefer-arrow": "^1.2.2",
 | 
			
		||||
    "eslint-plugin-promise": "^4.2.1",
 | 
			
		||||
    "faker": "^5.1.0",
 | 
			
		||||
    "fs-extra": "^9.1.0",
 | 
			
		||||
    "gulp": "4.0.2",
 | 
			
		||||
    "gulp-clip-empty-files": "^0.1.2",
 | 
			
		||||
    "gulp-concat": "^2.6.1",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								scripts/copy-assets.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								scripts/copy-assets.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
// (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.
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Script to copy some files to the www folder.
 | 
			
		||||
 */
 | 
			
		||||
const fse = require('fs-extra');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
// Assets to copy.
 | 
			
		||||
const ASSETS = {
 | 
			
		||||
    '/node_modules/mathjax/MathJax.js': '/lib/mathjax/MathJax.js',
 | 
			
		||||
    '/node_modules/mathjax/extensions': '/lib/mathjax/extensions',
 | 
			
		||||
    '/node_modules/mathjax/jax/element': '/lib/mathjax/jax/element',
 | 
			
		||||
    '/node_modules/mathjax/jax/input': '/lib/mathjax/jax/input',
 | 
			
		||||
    '/node_modules/mathjax/jax/output/SVG': '/lib/mathjax/jax/output/SVG',
 | 
			
		||||
    '/node_modules/mathjax/jax/output/PreviewHTML': '/lib/mathjax/jax/output/PreviewHTML',
 | 
			
		||||
    '/node_modules/mathjax/localization': '/lib/mathjax/localization',
 | 
			
		||||
    '/src/core/features/h5p/assets': '/lib/h5p',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = function(ctx) {
 | 
			
		||||
    const assetsPath = ctx.project.srcDir + '/assets';
 | 
			
		||||
 | 
			
		||||
    for (const src in ASSETS) {
 | 
			
		||||
        fse.copySync(ctx.project.dir + src, assetsPath + ASSETS[src], { overwrite: true });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@ -245,7 +245,7 @@ export class AddonFilterMathJaxLoaderHandlerService extends CoreFilterDefaultHan
 | 
			
		||||
 | 
			
		||||
                    const equations = Array.from(container.querySelectorAll('.filter_mathjaxloader_equation'));
 | 
			
		||||
                    equations.forEach((node) => {
 | 
			
		||||
                        that.window.MathJax.Hub.Queue(['Typeset', that.window.MathJax.Hub, node]);
 | 
			
		||||
                        that.window.MathJax.Hub.Queue(['Typeset', that.window.MathJax.Hub, node], [that.fixUseUrls, node]);
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    // Set the delay back to normal after processing.
 | 
			
		||||
@ -255,6 +255,20 @@ export class AddonFilterMathJaxLoaderHandlerService extends CoreFilterDefaultHan
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fix URLs in <use> elements.
 | 
			
		||||
     * This is needed because MathJax stores the location.href when it's loaded, and then sets that URL to all the <use>
 | 
			
		||||
     * elements href. Since the app URL changes when navigating, the SVGs can use a URL that isn't the current page.
 | 
			
		||||
     * When that happens, the request returns a 404 error and the SVG isn't displayed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param node Element that can contain equations.
 | 
			
		||||
     */
 | 
			
		||||
    protected fixUseUrls(node: Element): void {
 | 
			
		||||
        Array.from(node.querySelectorAll('use')).forEach((useElem) => {
 | 
			
		||||
            useElem.setAttribute('href', useElem.href.baseVal.substr(useElem.href.baseVal.indexOf('#')));
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform a mapping of the app language code to the equivalent for MathJax.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								src/addons/mod/h5pactivity/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/addons/mod/h5pactivity/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
// (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 { NgModule } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
 | 
			
		||||
import { AddonModH5PActivityIndexComponent } from './index';
 | 
			
		||||
import { CoreH5PComponentsModule } from '@features/h5p/components/components.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModH5PActivityIndexComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        CoreCourseComponentsModule,
 | 
			
		||||
        CoreH5PComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModH5PActivityIndexComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityComponentsModule {}
 | 
			
		||||
@ -0,0 +1,91 @@
 | 
			
		||||
<!-- Buttons to add to the header. -->
 | 
			
		||||
<core-navbar-buttons slot="end">
 | 
			
		||||
    <core-context-menu>
 | 
			
		||||
        <core-context-menu-item *ngIf="h5pActivity && h5pActivity.enabletracking && accessInfo && !accessInfo.canreviewattempts"
 | 
			
		||||
            [priority]="1000" [content]="'addon.mod_h5pactivity.review_my_attempts' | translate" (action)="viewMyAttempts()"
 | 
			
		||||
            iconAction="stats-chart">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
 | 
			
		||||
            [href]="externalUrl" iconAction="fas-external-link-alt">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
 | 
			
		||||
            (action)="expandDescription()" iconAction="fas-arrow-right">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
 | 
			
		||||
            iconAction="far-newspaper" (action)="gotoBlog()">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
 | 
			
		||||
            (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)"
 | 
			
		||||
            [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
 | 
			
		||||
            [iconAction]="prefetchStatusIcon" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
 | 
			
		||||
            iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
    </core-context-menu>
 | 
			
		||||
</core-navbar-buttons>
 | 
			
		||||
 | 
			
		||||
<!-- Content. -->
 | 
			
		||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
 | 
			
		||||
 | 
			
		||||
    <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
 | 
			
		||||
        contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
 | 
			
		||||
    </core-course-module-description>
 | 
			
		||||
 | 
			
		||||
    <!-- Offline data stored. -->
 | 
			
		||||
    <ion-card class="core-warning-card" *ngIf="hasOffline">
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
            <ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <!-- Offline disabled. -->
 | 
			
		||||
    <ion-card class="core-warning-card" *ngIf="!siteCanDownload && playing">
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <!-- Preview mode. -->
 | 
			
		||||
    <ion-card class="core-warning-card" *ngIf="accessInfo && !trackComponent">
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                {{ 'addon.mod_h5pactivity.previewmode' | translate }}
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <ion-list *ngIf="deployedFile && !playing">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="stateMessage">
 | 
			
		||||
            <ion-label>{{ stateMessage | translate }}</ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <!-- Button to download the package. -->
 | 
			
		||||
        <ion-button *ngIf="!downloading && needsDownload" class="ion-text-wrap ion-margin" expand="block"
 | 
			
		||||
            (click)="downloadAndPlay($event)">
 | 
			
		||||
            {{ 'addon.mod_h5pactivity.downloadh5pfile' | translate }}
 | 
			
		||||
        </ion-button>
 | 
			
		||||
 | 
			
		||||
        <!-- Download progress. -->
 | 
			
		||||
        <ion-item class="ion-text-center" *ngIf="downloading">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <ion-spinner></ion-spinner>
 | 
			
		||||
                <h2 *ngIf="progressMessage">{{ progressMessage | translate }}</h2>
 | 
			
		||||
                <core-progress-bar *ngIf="showPercentage" [progress]="percentage"></core-progress-bar>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-list>
 | 
			
		||||
 | 
			
		||||
    <core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"
 | 
			
		||||
        [trackComponent]="trackComponent" [contextId]="h5pActivity?.context">
 | 
			
		||||
    </core-h5p-iframe>
 | 
			
		||||
</core-loading>
 | 
			
		||||
							
								
								
									
										495
									
								
								src/addons/mod/h5pactivity/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										495
									
								
								src/addons/mod/h5pactivity/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,495 @@
 | 
			
		||||
// (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 { Component, Optional, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreSite } from '@classes/site';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreH5PDisplayOptions } from '@features/h5p/classes/core';
 | 
			
		||||
import { CoreH5PHelper } from '@features/h5p/classes/helper';
 | 
			
		||||
import { CoreH5P } from '@features/h5p/services/h5p';
 | 
			
		||||
import { CoreXAPIOffline } from '@features/xapi/services/offline';
 | 
			
		||||
import { CoreXAPI } from '@features/xapi/services/xapi';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreFilepool } from '@services/filepool';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModH5PActivity,
 | 
			
		||||
    AddonModH5PActivityAccessInfo,
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityProvider,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModH5PActivitySync,
 | 
			
		||||
    AddonModH5PActivitySyncProvider,
 | 
			
		||||
    AddonModH5PActivitySyncResult,
 | 
			
		||||
} from '../../services/h5pactivity-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays an H5P activity entry page.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-h5pactivity-index',
 | 
			
		||||
    templateUrl: 'addon-mod-h5pactivity-index.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    component = AddonModH5PActivityProvider.COMPONENT;
 | 
			
		||||
    moduleName = 'h5pactivity';
 | 
			
		||||
 | 
			
		||||
    h5pActivity?: AddonModH5PActivityData; // The H5P activity object.
 | 
			
		||||
    accessInfo?: AddonModH5PActivityAccessInfo; // Info about the user capabilities.
 | 
			
		||||
    deployedFile?: CoreWSExternalFile; // The H5P deployed file.
 | 
			
		||||
 | 
			
		||||
    stateMessage?: string; // Message about the file state.
 | 
			
		||||
    downloading = false; // Whether the H5P file is being downloaded.
 | 
			
		||||
    needsDownload = false; // Whether the file needs to be downloaded.
 | 
			
		||||
    percentage?: string; // Download/unzip percentage.
 | 
			
		||||
    showPercentage = false; // Whether to show the percentage.
 | 
			
		||||
    progressMessage?: string; // Message about download/unzip.
 | 
			
		||||
    playing = false; // Whether the package is being played.
 | 
			
		||||
    displayOptions?: CoreH5PDisplayOptions; // Display options for the package.
 | 
			
		||||
    onlinePlayerUrl?: string; // URL to play the package in online.
 | 
			
		||||
    fileUrl?: string; // The fileUrl to use to play the package.
 | 
			
		||||
    state?: string; // State of the file.
 | 
			
		||||
    siteCanDownload = false;
 | 
			
		||||
    trackComponent?: string; // Component for tracking.
 | 
			
		||||
    hasOffline = false;
 | 
			
		||||
    isOpeningPage = false;
 | 
			
		||||
 | 
			
		||||
    protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
 | 
			
		||||
    protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
 | 
			
		||||
    protected site: CoreSite;
 | 
			
		||||
    protected observer?: CoreEventObserver;
 | 
			
		||||
    protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected content?: IonContent,
 | 
			
		||||
        @Optional() courseContentsPage?: CoreCourseContentsPage,
 | 
			
		||||
    ) {
 | 
			
		||||
        super('AddonModH5PActivityIndexComponent', content, courseContentsPage);
 | 
			
		||||
 | 
			
		||||
        this.site = CoreSites.getCurrentSite()!;
 | 
			
		||||
        this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite();
 | 
			
		||||
 | 
			
		||||
        // Listen for messages from the iframe.
 | 
			
		||||
        this.messageListenerFunction = this.onIframeMessage.bind(this);
 | 
			
		||||
        window.addEventListener('message', this.messageListenerFunction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        this.loadContent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.module.id, {
 | 
			
		||||
                siteId: this.siteId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.dataRetrieved.emit(this.h5pActivity);
 | 
			
		||||
            this.description = this.h5pActivity.intro;
 | 
			
		||||
            this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
 | 
			
		||||
 | 
			
		||||
            if (sync) {
 | 
			
		||||
                await this.syncActivity(showErrors);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                this.checkHasOffline(),
 | 
			
		||||
                this.fetchAccessInfo(),
 | 
			
		||||
                this.fetchDeployedFileData(),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            this.trackComponent = this.accessInfo?.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : '';
 | 
			
		||||
 | 
			
		||||
            if (this.h5pActivity.package && this.h5pActivity.package[0]) {
 | 
			
		||||
                // The online player should use the original file, not the trusted one.
 | 
			
		||||
                this.onlinePlayerUrl = CoreH5P.h5pPlayer.calculateOnlinePlayerUrl(
 | 
			
		||||
                    this.site.getURL(),
 | 
			
		||||
                    this.h5pActivity.package[0].fileurl,
 | 
			
		||||
                    this.displayOptions,
 | 
			
		||||
                    this.trackComponent,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) {
 | 
			
		||||
                // Cannot download the file or already downloaded, play the package directly.
 | 
			
		||||
                this.play();
 | 
			
		||||
 | 
			
		||||
            } else if ((this.state == CoreConstants.NOT_DOWNLOADED || this.state == CoreConstants.OUTDATED) && CoreApp.isOnline() &&
 | 
			
		||||
                    this.deployedFile?.filesize && CoreFilepool.shouldDownload(this.deployedFile.filesize)) {
 | 
			
		||||
                // Package is small, download it automatically. Don't block this function for this.
 | 
			
		||||
                this.downloadAutomatically();
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.fillContextMenu(refresh);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the access info and store it in the right variables.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async checkHasOffline(): Promise<void> {
 | 
			
		||||
        this.hasOffline = await CoreXAPIOffline.contextHasStatements(this.h5pActivity!.context, this.siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the access info and store it in the right variables.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchAccessInfo(): Promise<void> {
 | 
			
		||||
        this.accessInfo = await AddonModH5PActivity.getAccessInformation(this.h5pActivity!.id, {
 | 
			
		||||
            cmId: this.module.id,
 | 
			
		||||
            siteId: this.siteId,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the deployed file data if needed and store it in the right variables.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchDeployedFileData(): Promise<void> {
 | 
			
		||||
        if (!this.siteCanDownload) {
 | 
			
		||||
            // Cannot download the file, no need to fetch the file data.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity!, {
 | 
			
		||||
            displayOptions: this.displayOptions,
 | 
			
		||||
            siteId: this.siteId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.fileUrl = this.deployedFile.fileurl;
 | 
			
		||||
 | 
			
		||||
        // Listen for changes in the state.
 | 
			
		||||
        const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.deployedFile.fileurl);
 | 
			
		||||
 | 
			
		||||
        if (!this.observer) {
 | 
			
		||||
            this.observer = CoreEvents.on(eventName, () => {
 | 
			
		||||
                this.calculateFileState();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.calculateFileState();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate the state of the deployed file.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async calculateFileState(): Promise<void> {
 | 
			
		||||
        this.state = await CoreFilepool.getFileStateByUrl(
 | 
			
		||||
            this.site.getId(),
 | 
			
		||||
            this.deployedFile!.fileurl,
 | 
			
		||||
            this.deployedFile!.timemodified,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.showFileState();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected invalidateContent(): Promise<void> {
 | 
			
		||||
        return AddonModH5PActivity.invalidateActivityData(this.courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Displays some data based on the state of the main file.
 | 
			
		||||
     */
 | 
			
		||||
    protected async showFileState(): Promise<void> {
 | 
			
		||||
        if (this.state == CoreConstants.OUTDATED) {
 | 
			
		||||
            this.stateMessage = 'addon.mod_h5pactivity.filestateoutdated';
 | 
			
		||||
            this.needsDownload = true;
 | 
			
		||||
        } else if (this.state == CoreConstants.NOT_DOWNLOADED) {
 | 
			
		||||
            this.stateMessage = 'addon.mod_h5pactivity.filestatenotdownloaded';
 | 
			
		||||
            this.needsDownload = true;
 | 
			
		||||
        } else if (this.state == CoreConstants.DOWNLOADING) {
 | 
			
		||||
            this.stateMessage = '';
 | 
			
		||||
 | 
			
		||||
            if (!this.downloading) {
 | 
			
		||||
                // It's being downloaded right now but the view isn't tracking it. "Restore" the download.
 | 
			
		||||
                await this.downloadDeployedFile();
 | 
			
		||||
 | 
			
		||||
                this.play();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            this.stateMessage = '';
 | 
			
		||||
            this.needsDownload = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download the file and play it.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Click event.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async downloadAndPlay(event?: MouseEvent): Promise<void> {
 | 
			
		||||
        event?.preventDefault();
 | 
			
		||||
        event?.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        if (!CoreApp.isOnline()) {
 | 
			
		||||
            CoreDomUtils.showErrorModal('core.networkerrormsg', true);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Confirm the download if needed.
 | 
			
		||||
            await CoreDomUtils.confirmDownloadSize({ size: this.deployedFile!.filesize!, total: true });
 | 
			
		||||
 | 
			
		||||
            await this.downloadDeployedFile();
 | 
			
		||||
 | 
			
		||||
            if (!this.isDestroyed) {
 | 
			
		||||
                this.play();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (CoreDomUtils.isCanceledError(error) || this.isDestroyed) {
 | 
			
		||||
                // User cancelled or view destroyed, stop.
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download the file automatically.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async downloadAutomatically(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            await this.downloadDeployedFile();
 | 
			
		||||
 | 
			
		||||
            if (!this.isDestroyed) {
 | 
			
		||||
                this.play();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download athe H5P deployed file or restores an ongoing download.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async downloadDeployedFile(): Promise<void> {
 | 
			
		||||
        this.downloading = true;
 | 
			
		||||
        this.progressMessage = 'core.downloading';
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreFilepool.downloadUrl(
 | 
			
		||||
                this.site.getId(),
 | 
			
		||||
                this.deployedFile!.fileurl,
 | 
			
		||||
                false,
 | 
			
		||||
                this.component,
 | 
			
		||||
                this.componentId,
 | 
			
		||||
                this.deployedFile!.timemodified,
 | 
			
		||||
                (data: DownloadProgressData) => {
 | 
			
		||||
                    if (!data) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    this.percentage = undefined;
 | 
			
		||||
                    this.showPercentage = false;
 | 
			
		||||
 | 
			
		||||
                    if (data.message) {
 | 
			
		||||
                        // Show a message.
 | 
			
		||||
                        this.progressMessage = data.message;
 | 
			
		||||
                    } else if (data.loaded !== undefined) {
 | 
			
		||||
                        // Downloading or unzipping.
 | 
			
		||||
                        const totalSize = this.progressMessage == 'core.downloading' ? this.deployedFile!.filesize : data.total;
 | 
			
		||||
 | 
			
		||||
                        if (totalSize !== undefined) {
 | 
			
		||||
                            const percentageNumber = (Number(data.loaded / totalSize) * 100);
 | 
			
		||||
                            this.percentage = percentageNumber.toFixed(1);
 | 
			
		||||
                            this.showPercentage = percentageNumber >= 0 && percentageNumber <= 100;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.progressMessage = undefined;
 | 
			
		||||
            this.percentage = undefined;
 | 
			
		||||
            this.showPercentage = false;
 | 
			
		||||
            this.downloading = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Play the package.
 | 
			
		||||
     */
 | 
			
		||||
    play(): void {
 | 
			
		||||
        this.playing = true;
 | 
			
		||||
 | 
			
		||||
        // Mark the activity as viewed.
 | 
			
		||||
        AddonModH5PActivity.logView(this.h5pActivity!.id, this.h5pActivity!.name, this.siteId);
 | 
			
		||||
 | 
			
		||||
        CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to view user events.
 | 
			
		||||
     */
 | 
			
		||||
    async viewMyAttempts(): Promise<void> {
 | 
			
		||||
        this.isOpeningPage = true;
 | 
			
		||||
        const userId = CoreSites.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreNavigator.navigate(`userattempts/${userId}`);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.isOpeningPage = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Treat an iframe message event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async onIframeMessage(event: MessageEvent): Promise<void> {
 | 
			
		||||
        if (!event.data || !CoreXAPI.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const options = {
 | 
			
		||||
                offline: this.hasOffline,
 | 
			
		||||
                courseId: this.courseId,
 | 
			
		||||
                extra: this.h5pActivity!.name,
 | 
			
		||||
                siteId: this.site.getId(),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const sent = await CoreXAPI.postStatements(
 | 
			
		||||
                this.h5pActivity!.context,
 | 
			
		||||
                event.data.component,
 | 
			
		||||
                JSON.stringify(event.data.statements),
 | 
			
		||||
                options,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            this.hasOffline = !sent;
 | 
			
		||||
 | 
			
		||||
            if (sent) {
 | 
			
		||||
                try {
 | 
			
		||||
                    // Invalidate attempts.
 | 
			
		||||
                    await AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity!.id, undefined, this.siteId);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    // Ignore errors.
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error sending tracking data.');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if an event is an XAPI post statement of the current activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param data Event data.
 | 
			
		||||
     * @return Whether it's an XAPI post statement of the current activity.
 | 
			
		||||
     */
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    protected isCurrentXAPIPost(data: any): boolean {
 | 
			
		||||
        if (data.environment != 'moodleapp' || data.context != 'h5p' || data.action != 'xapi_post_statement' || !data.statements) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check the event belongs to this activity.
 | 
			
		||||
        const trackingUrl = data.statements[0] && data.statements[0].object && data.statements[0].object.id;
 | 
			
		||||
        if (!trackingUrl) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.site.containsUrl(trackingUrl)) {
 | 
			
		||||
            // The event belongs to another site, weird scenario. Maybe some JS running in background.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const match = trackingUrl.match(/xapi\/activity\/(\d+)/);
 | 
			
		||||
 | 
			
		||||
        return match && match[1] == this.h5pActivity!.context;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected sync(): Promise<AddonModH5PActivitySyncResult> {
 | 
			
		||||
        return AddonModH5PActivitySync.syncActivity(this.h5pActivity!.context, this.site.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected autoSyncEventReceived(): void {
 | 
			
		||||
        this.checkHasOffline();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async gotoBlog(): Promise<void> {
 | 
			
		||||
        this.isOpeningPage = true;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await super.gotoBlog();
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.isOpeningPage = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        super.ngOnDestroy();
 | 
			
		||||
 | 
			
		||||
        this.observer?.off();
 | 
			
		||||
        window.removeEventListener('message', this.messageListenerFunction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DownloadProgressData = {
 | 
			
		||||
    message?: string;
 | 
			
		||||
    loaded?: number;
 | 
			
		||||
    total?: number;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										51
									
								
								src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
// (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 { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CanLeaveGuard } from '@guards/can-leave';
 | 
			
		||||
import { AddonModH5PActivityComponentsModule } from './components/components.module';
 | 
			
		||||
import { AddonModH5PActivityIndexPage } from './pages/index/index';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId',
 | 
			
		||||
        component: AddonModH5PActivityIndexPage,
 | 
			
		||||
        canDeactivate: [CanLeaveGuard],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/userattempts/:userId',
 | 
			
		||||
        loadChildren: () => import('./pages/user-attempts/user-attempts.module')
 | 
			
		||||
            .then( m => m.AddonModH5PActivityUserAttemptsPageModule),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/attemptresults/:attemptId',
 | 
			
		||||
        loadChildren: () => import('./pages/attempt-results/attempt-results.module')
 | 
			
		||||
            .then( m => m.AddonModH5PActivityAttemptResultsPageModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        AddonModH5PActivityComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModH5PActivityIndexPage,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityLazyModule {}
 | 
			
		||||
							
								
								
									
										64
									
								
								src/addons/mod/h5pactivity/h5pactivity.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/addons/mod/h5pactivity/h5pactivity.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core';
 | 
			
		||||
import { Routes } from '@angular/router';
 | 
			
		||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
			
		||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
import { CoreCronDelegate } from '@services/cron';
 | 
			
		||||
import { AddonModH5PActivityComponentsModule } from './components/components.module';
 | 
			
		||||
import { AddonModH5PActivityProvider } from './services/h5pactivity';
 | 
			
		||||
import { AddonModH5PActivitySyncProvider } from './services/h5pactivity-sync';
 | 
			
		||||
import { AddonModH5PActivityIndexLinkHandler } from './services/handlers/index-link';
 | 
			
		||||
import { AddonModH5PActivityModuleHandler, AddonModH5PActivityModuleHandlerService } from './services/handlers/module';
 | 
			
		||||
import { AddonModH5PActivityPrefetchHandler } from './services/handlers/prefetch';
 | 
			
		||||
import { AddonModH5PActivityReportLinkHandler } from './services/handlers/report-link';
 | 
			
		||||
import { AddonModH5PActivitySyncCronHandler } from './services/handlers/sync-cron';
 | 
			
		||||
 | 
			
		||||
// List of providers (without handlers).
 | 
			
		||||
export const ADDON_MOD_H5P_ACTIVITY_SERVICES: Type<unknown>[] = [
 | 
			
		||||
    AddonModH5PActivityProvider,
 | 
			
		||||
    AddonModH5PActivitySyncProvider,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: AddonModH5PActivityModuleHandlerService.PAGE_NAME,
 | 
			
		||||
        loadChildren: () => import('./h5pactivity-lazy.module').then(m => m.AddonModH5PActivityLazyModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild(routes),
 | 
			
		||||
        AddonModH5PActivityComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                CoreCourseModuleDelegate.registerHandler(AddonModH5PActivityModuleHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModH5PActivityIndexLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModH5PActivityReportLinkHandler.instance);
 | 
			
		||||
                CoreCourseModulePrefetchDelegate.registerHandler(AddonModH5PActivityPrefetchHandler.instance);
 | 
			
		||||
                CoreCronDelegate.register(AddonModH5PActivitySyncCronHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityModule {}
 | 
			
		||||
							
								
								
									
										36
									
								
								src/addons/mod/h5pactivity/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/addons/mod/h5pactivity/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
{
 | 
			
		||||
    "all_attempts": "All user attempts",
 | 
			
		||||
    "answer_checked": "Answer checked",
 | 
			
		||||
    "answer_correct": "Your answer is correct",
 | 
			
		||||
    "answer_fail": "Incorrect answer",
 | 
			
		||||
    "answer_incorrect": "Your answer is incorrect",
 | 
			
		||||
    "answer_pass": "Correct answer",
 | 
			
		||||
    "attempt": "Attempt",
 | 
			
		||||
    "attempt_completion_no": "This attempt is not marked as completed",
 | 
			
		||||
    "attempt_completion_yes": "This attempt is completed",
 | 
			
		||||
    "attempt_success_fail": "Fail",
 | 
			
		||||
    "attempt_success_pass": "Pass",
 | 
			
		||||
    "attempt_success_unknown": "Not reported",
 | 
			
		||||
    "attempts_none": "This user has no attempts to display.",
 | 
			
		||||
    "completion": "Completion",
 | 
			
		||||
    "downloadh5pfile": "Download H5P file",
 | 
			
		||||
    "duration": "Duration",
 | 
			
		||||
    "errorgetactivity": "Error getting H5P activity data.",
 | 
			
		||||
    "filestatenotdownloaded": "The H5P package is not downloaded. You need to download it to be able to use it.",
 | 
			
		||||
    "filestateoutdated": "The H5P package has been modified since the last download. You need to download it again to be able to use it.",
 | 
			
		||||
    "maxscore": "Max score",
 | 
			
		||||
    "modulenameplural": "H5P",
 | 
			
		||||
    "myattempts": "My attempts",
 | 
			
		||||
    "no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking\n        provided is not compatible with the current activity version.",
 | 
			
		||||
    "offlinedisabledwarning": "You need to be online to view the H5P package.",
 | 
			
		||||
    "outcome": "Outcome",
 | 
			
		||||
    "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
 | 
			
		||||
    "result_fill-in": "Fill-in text",
 | 
			
		||||
    "result_other": "Unknown interaction type",
 | 
			
		||||
    "review_my_attempts": "View my attempts",
 | 
			
		||||
    "score": "Score",
 | 
			
		||||
    "score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}",
 | 
			
		||||
    "startdate": "Start date",
 | 
			
		||||
    "totalscore": "Total score",
 | 
			
		||||
    "viewattempt": "View attempt {{$a}}"
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,193 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text *ngIf="h5pActivity" [text]="h5pActivity.name" contextLevel="module"
 | 
			
		||||
                [contextInstanceId]="h5pActivity.coursemodule" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <ng-container *ngIf="attempt">
 | 
			
		||||
            <!-- Attempt number and user that did the attempt. -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="user" core-user-link [userId]="user.id" [courseId]="courseId"
 | 
			
		||||
                [title]="user.fullname">
 | 
			
		||||
                <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}: {{user.fullname}}</h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <!-- Attempt number (if user not known). -->
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="!user">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <h2>{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}</h2>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <!-- Attempt summary. -->
 | 
			
		||||
            <ion-card class="addon-mod_h5pactivity-attempt-result-summary">
 | 
			
		||||
                <ion-list>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.startdate' | translate }}</h2>
 | 
			
		||||
                            <p>{{ attempt.timecreated | coreFormatDate:'strftimedatetime' }}</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.completion' | translate }}</h2>
 | 
			
		||||
                            <p *ngIf="attempt.completion">
 | 
			
		||||
                                <img src="assets/img/completion/completion-auto-y.svg" role="presentation" alt="">
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_completion_yes' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="!attempt.completion">
 | 
			
		||||
                                <img src="assets/img/completion/completion-auto-n.svg" role="presentation" alt="">
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_completion_no' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.duration' | translate }}</h2>
 | 
			
		||||
                            <p>{{ attempt.durationReadable }}</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.outcome' | translate }}</h2>
 | 
			
		||||
                            <p *ngIf="attempt.success !== null && attempt.success" >
 | 
			
		||||
                                <ion-icon name="fa-check-circle"></ion-icon>
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_success_pass' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="attempt.success !== null && !attempt.success" >
 | 
			
		||||
                                <ion-icon name="far-circle"></ion-icon>
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_success_fail' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="attempt.success === null" >
 | 
			
		||||
                                {{ 'addon.mod_h5pactivity.attempt_success_unknown' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item *ngIf="attempt.maxscore" class="ion-text-wrap" lines="none">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <h2>{{ 'addon.mod_h5pactivity.totalscore' | translate }}</h2>
 | 
			
		||||
                            <p>{{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: attempt} }}</p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ion-list>
 | 
			
		||||
            </ion-card>
 | 
			
		||||
 | 
			
		||||
            <!-- Results. -->
 | 
			
		||||
            <ng-container *ngIf="attempt.results">
 | 
			
		||||
                <ion-card *ngFor="let result of attempt.results">
 | 
			
		||||
                    <ion-card-header class="ion-text-wrap">
 | 
			
		||||
                        <ion-card-title>
 | 
			
		||||
                            <core-format-text [text]="result.description" [component]="component" [componentId]="cmId"
 | 
			
		||||
                                contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </ion-card-title>
 | 
			
		||||
                    </ion-card-header>
 | 
			
		||||
                    <ion-item *ngIf="result.content" class="ion-text-wrap">
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <core-format-text [text]="result.content" [component]="component" [componentId]="cmId"
 | 
			
		||||
                                contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
 | 
			
		||||
                    <!-- Options. -->
 | 
			
		||||
                    <ng-container *ngIf="result.options && result.options.length">
 | 
			
		||||
                        <ion-item class="ion-text-wrap addon-mod_h5pactivity-result-table-header">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <ion-row class="ion-align-items-center">
 | 
			
		||||
                                    <ion-col class="ion-text-center">{{ result.optionslabel }}</ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">{{ result.correctlabel }}</ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">{{ result.answerlabel }}</ion-col>
 | 
			
		||||
                                </ion-row>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                        <ion-item *ngFor="let option of result.options"
 | 
			
		||||
                            class="ion-text-wrap addon-mod_h5pactivity-result-table-row">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <ion-row class="ion-align-items-center">
 | 
			
		||||
                                    <ion-col class="ion-text-center">
 | 
			
		||||
                                        <core-format-text [text]="option.description" [component]="component" [componentId]="cmId"
 | 
			
		||||
                                            contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
                                        </core-format-text>
 | 
			
		||||
                                    </ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">
 | 
			
		||||
                                        <ng-container *ngIf="option.correctanswer">
 | 
			
		||||
                                            <ng-container
 | 
			
		||||
                                                *ngTemplateOutlet="answerTemplate; context: {answer: option.correctanswer}">
 | 
			
		||||
                                            </ng-container>
 | 
			
		||||
                                        </ng-container>
 | 
			
		||||
                                    </ion-col>
 | 
			
		||||
                                    <ion-col class="ion-text-center">
 | 
			
		||||
                                        <ng-container *ngIf="option.useranswer">
 | 
			
		||||
                                            <ng-container *ngTemplateOutlet="answerTemplate; context: {answer: option.useranswer}">
 | 
			
		||||
                                            </ng-container>
 | 
			
		||||
                                        </ng-container>
 | 
			
		||||
                                    </ion-col>
 | 
			
		||||
                                </ion-row>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
 | 
			
		||||
                        <!-- Result score. -->
 | 
			
		||||
                        <ion-item *ngIf="result.maxscore" class="ion-text-wrap ion-text-end addon-mod_h5pactivity-result-score">
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <p><strong>
 | 
			
		||||
                                    {{ 'addon.mod_h5pactivity.score' | translate }}:
 | 
			
		||||
                                     {{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: result} }}
 | 
			
		||||
                                </strong></p>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
 | 
			
		||||
                    <!-- Result doesn't support tracking. -->
 | 
			
		||||
                    <ion-item class="ion-text-wrap core-warning-item" *ngIf="!result.track" lines="none">
 | 
			
		||||
                        <ion-icon slot="start" name="fas-exclamation-triangle" color="warning"></ion-icon>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            {{ 'addon.mod_h5pactivity.no_compatible_track' | translate:{$a: result.interactiontype} }}
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ion-card>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
<!-- Template to render an answer. -->
 | 
			
		||||
<ng-template #answerTemplate let-answer="answer">
 | 
			
		||||
    <p *ngIf="answer.correct">
 | 
			
		||||
        <ion-icon name="fa-check" [attr.aria-label]="'addon.mod_h5pactivity.answer_correct' | translate" color="success">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
        {{ answer.answer }}
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.incorrect">
 | 
			
		||||
        <ion-icon name="fa-remove" [attr.aria-label]="'addon.mod_h5pactivity.answer_incorrect' | translate" color="danger">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
        {{ answer.answer }}
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.text">
 | 
			
		||||
        {{ answer.answer }}
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.checked">
 | 
			
		||||
        <ion-icon name="fa-check-circle" [attr.aria-label]="'addon.mod_h5pactivity.answer_checked' | translate">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.pass">
 | 
			
		||||
        <ion-icon name="fa-check" [attr.aria-label]="'addon.mod_h5pactivity.answer_pass' | translate" color="success">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </p>
 | 
			
		||||
    <p *ngIf="answer.fail">
 | 
			
		||||
        <ion-icon name="fa-remove" [attr.aria-label]="'addon.mod_h5pactivity.answer_fail' | translate" color="danger">
 | 
			
		||||
        </ion-icon>
 | 
			
		||||
    </p>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
// (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 { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { AddonModH5PActivityAttemptResultsPage } from './attempt-results';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonModH5PActivityAttemptResultsPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModH5PActivityAttemptResultsPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityAttemptResultsPageModule {}
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    .core-warning-item {
 | 
			
		||||
        --inner-border-width: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-attempt-result-summary {
 | 
			
		||||
        img {
 | 
			
		||||
            width: 16px;
 | 
			
		||||
            height: 16px;
 | 
			
		||||
            display: inline;
 | 
			
		||||
            @include margin-horizontal(0, 4px);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-attempt-result-summary,
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-header,
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-row {
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            font-size: 1.2em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-header {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-row.item:nth-child(even) {
 | 
			
		||||
        --background: var(--gray-lighter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-result-score {
 | 
			
		||||
        border-top: 1px solid black;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(body.dark) {
 | 
			
		||||
    .addon-mod_h5pactivity-result-table-row.item:nth-child(even) {
 | 
			
		||||
        --background: var(--black);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,124 @@
 | 
			
		||||
// (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 { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModH5PActivity,
 | 
			
		||||
    AddonModH5PActivityProvider,
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityAttemptResults,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays results of an attempt.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-h5pactivity-attempt-results',
 | 
			
		||||
    templateUrl: 'attempt-results.html',
 | 
			
		||||
    styleUrls: ['attempt-results.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityAttemptResultsPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    h5pActivity?: AddonModH5PActivityData;
 | 
			
		||||
    attempt?: AddonModH5PActivityAttemptResults;
 | 
			
		||||
    user?: CoreUserProfile;
 | 
			
		||||
    component = AddonModH5PActivityProvider.COMPONENT;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    cmId!: number;
 | 
			
		||||
 | 
			
		||||
    protected attemptId!: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
 | 
			
		||||
        this.attemptId = CoreNavigator.getRouteNumberParam('attemptId')!;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     */
 | 
			
		||||
    doRefresh(refresher: IonRefresher): void {
 | 
			
		||||
        this.refreshData().finally(() => {
 | 
			
		||||
            refresher.complete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get quiz data and attempt data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
 | 
			
		||||
        this.attempt = await AddonModH5PActivity.getAttemptResults(this.h5pActivity.id, this.attemptId, {
 | 
			
		||||
            cmId: this.cmId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await this.fetchUserProfile();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get user profile.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchUserProfile(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            this.user = await CoreUser.getProfile(this.attempt!.userid, this.courseId, true);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshData(): Promise<void> {
 | 
			
		||||
        const promises = [
 | 
			
		||||
            AddonModH5PActivity.invalidateActivityData(this.courseId),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (this.h5pActivity) {
 | 
			
		||||
            promises.push(AddonModH5PActivity.invalidateAttemptResults(this.h5pActivity.id, this.attemptId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(Promise.all(promises));
 | 
			
		||||
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								src/addons/mod/h5pactivity/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/addons/mod/h5pactivity/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <!-- The buttons defined by the component will be added in here. -->
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
 | 
			
		||||
    <addon-mod-h5pactivity-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)">
 | 
			
		||||
    </addon-mod-h5pactivity-index>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										52
									
								
								src/addons/mod/h5pactivity/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/addons/mod/h5pactivity/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
// (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 { Component, ViewChild } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
 | 
			
		||||
import { CanLeave } from '@guards/can-leave';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { AddonModH5PActivityIndexComponent } from '../../components/index';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays an H5P activity.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-h5pactivity-index',
 | 
			
		||||
    templateUrl: 'index.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityIndexPage extends CoreCourseModuleMainActivityPage<AddonModH5PActivityIndexComponent>
 | 
			
		||||
    implements CanLeave {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(AddonModH5PActivityIndexComponent) activityComponent?: AddonModH5PActivityIndexComponent;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async canLeave(): Promise<boolean> {
 | 
			
		||||
        if (!this.activityComponent || !this.activityComponent.playing || this.activityComponent.isOpeningPage) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await CoreDomUtils.showConfirm(Translate.instant('core.confirmleaveunknownchanges'));
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,114 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text *ngIf="h5pActivity" [text]="h5pActivity.name" contextLevel="module"
 | 
			
		||||
                [contextInstanceId]="h5pActivity.coursemodule" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <!-- User viewed. -->
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="user && !isCurrentUser" core-user-link [userId]="user.id" [courseId]="courseId"
 | 
			
		||||
            [title]="user.fullname">
 | 
			
		||||
            <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <h2>{{ user.fullname }}</h2>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="user && isCurrentUser">
 | 
			
		||||
            <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <h2>{{ 'addon.mod_h5pactivity.myattempts' | translate }}</h2>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <ion-list *ngIf="attemptsData">
 | 
			
		||||
            <!-- Attempts used to calculate the score. -->
 | 
			
		||||
            <ng-container *ngIf="attemptsData.scored">
 | 
			
		||||
                <ion-item-divider>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ attemptsData.scored.title }}</h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
                <ng-container *ngTemplateOutlet="attemptsTemplate; context: {attempts: attemptsData.scored.attempts}">
 | 
			
		||||
                </ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
 | 
			
		||||
            <!-- All attempts. -->
 | 
			
		||||
            <ng-container *ngIf="attemptsData.attempts && attemptsData.attempts.length">
 | 
			
		||||
                <ion-item-divider>
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_h5pactivity.all_attempts' | translate }}</h2>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item-divider>
 | 
			
		||||
                <ng-container *ngTemplateOutlet="attemptsTemplate; context: {attempts: attemptsData.attempts}"></ng-container>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
 | 
			
		||||
        <!-- No attempts. -->
 | 
			
		||||
        <core-empty-box *ngIf="attemptsData && (!attemptsData.attempts || !attemptsData.attempts.length)" icon="stats-chart"
 | 
			
		||||
            [message]="'addon.mod_h5pactivity.attempts_none' | translate">
 | 
			
		||||
        </core-empty-box>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
<!-- Template to render a list of conversations. -->
 | 
			
		||||
<ng-template #attemptsTemplate let-attempts="attempts">
 | 
			
		||||
    <!-- "Header" of the table -->
 | 
			
		||||
    <ion-item class="ion-text-wrap addon-mod_h5pactivity-table-header" detail="true">
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <ion-row class="ion-align-items-center">
 | 
			
		||||
                <ion-col class="ion-text-center">#</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center" size="5" size-md="2">{{ 'core.date' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center">{{ 'addon.mod_h5pactivity.score' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ 'addon.mod_h5pactivity.maxscore' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ 'addon.mod_h5pactivity.duration' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ 'addon.mod_h5pactivity.completion' | translate }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center">{{ 'core.success' | translate }}</ion-col>
 | 
			
		||||
            </ion-row>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
 | 
			
		||||
    <!-- List of attempts. -->
 | 
			
		||||
    <ion-item class="ion-text-wrap addon-mod_h5pactivity-table-row" *ngFor="let attempt of attempts" button detail="true"
 | 
			
		||||
        [attr.aria-label]="'addon.mod_h5pactivity.viewattempt' | translate:{$a: attempt.attempt}" (click)="openAttempt(attempt)">
 | 
			
		||||
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <ion-row class="ion-align-items-center">
 | 
			
		||||
                <ion-col class="ion-text-center">{{ attempt.attempt }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center" size="5" size-md="2">
 | 
			
		||||
                    {{ attempt.timemodified | coreFormatDate:'strftimedatetimeshort' }}
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center">
 | 
			
		||||
                    {{ attempt.rawscore }}<span class="ion-hide-md-up"> / {{ attempt.maxscore }}</span>
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ attempt.maxscore }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">{{ attempt.durationReadable }}</ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center ion-hide-md-down">
 | 
			
		||||
                    <img *ngIf="attempt.completion" src="assets/img/completion/completion-auto-y.svg"
 | 
			
		||||
                        [alt]="'addon.mod_h5pactivity.attempt_completion_yes' | translate">
 | 
			
		||||
                    <img *ngIf="!attempt.completion" src="assets/img/completion/completion-auto-n.svg"
 | 
			
		||||
                        [alt]="'addon.mod_h5pactivity.attempt_completion_no' | translate">
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col class="ion-text-center addon-mod_h5pactivity-table-success-col">
 | 
			
		||||
                    <ion-icon *ngIf="attempt.success !== null && attempt.success" name="fa-check-circle"
 | 
			
		||||
                        [attr.aria-label]="'addon.mod_h5pactivity.attempt_success_pass' | translate">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <ion-icon *ngIf="attempt.success !== null && !attempt.success" name="far-circle"
 | 
			
		||||
                        [attr.aria-label]="'addon.mod_h5pactivity.attempt_success_fail' | translate">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
                    <img *ngIf="attempt.success === null" src="assets/img/icons/empty.svg"
 | 
			
		||||
                        [alt]="'addon.mod_h5pactivity.attempt_success_unknown' | translate">
 | 
			
		||||
                </ion-col>
 | 
			
		||||
            </ion-row>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
// (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 { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { AddonModH5PActivityUserAttemptsPage } from './user-attempts';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: AddonModH5PActivityUserAttemptsPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModH5PActivityUserAttemptsPage,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [RouterModule],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityUserAttemptsPageModule {}
 | 
			
		||||
@ -0,0 +1,10 @@
 | 
			
		||||
:host {
 | 
			
		||||
    .addon-mod_h5pactivity-table-header {
 | 
			
		||||
        --detail-icon-opacity: 0;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-mod_h5pactivity-table-row .addon-mod_h5pactivity-table-success-col {
 | 
			
		||||
        font-size: 1.2em;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										146
									
								
								src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,146 @@
 | 
			
		||||
// (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 { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModH5PActivity,
 | 
			
		||||
    AddonModH5PActivityAttempt,
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityUserAttempts,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays user attempts of a certain user.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-h5pactivity-user-attempts',
 | 
			
		||||
    templateUrl: 'user-attempts.html',
 | 
			
		||||
    styleUrls: ['user-attempts.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModH5PActivityUserAttemptsPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    cmId!: number;
 | 
			
		||||
    h5pActivity?: AddonModH5PActivityData;
 | 
			
		||||
    attemptsData?: AddonModH5PActivityUserAttempts;
 | 
			
		||||
    user?: CoreUserProfile;
 | 
			
		||||
    isCurrentUser = false;
 | 
			
		||||
 | 
			
		||||
    protected userId!: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
 | 
			
		||||
        this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId();
 | 
			
		||||
        this.isCurrentUser = this.userId == CoreSites.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     */
 | 
			
		||||
    doRefresh(refresher: IonRefresher): void {
 | 
			
		||||
        this.refreshData().finally(() => {
 | 
			
		||||
            refresher.complete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get quiz data and attempt data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            this.fetchAttempts(),
 | 
			
		||||
            this.fetchUserProfile(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get attempts.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchAttempts(): Promise<void> {
 | 
			
		||||
        this.attemptsData = await AddonModH5PActivity.getUserAttempts(this.h5pActivity!.id, {
 | 
			
		||||
            cmId: this.cmId,
 | 
			
		||||
            userId: this.userId,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get user profile.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchUserProfile(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            this.user = await CoreUser.getProfile(this.userId, this.courseId, true);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshData(): Promise<void> {
 | 
			
		||||
        const promises = [
 | 
			
		||||
            AddonModH5PActivity.invalidateActivityData(this.courseId),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (this.h5pActivity) {
 | 
			
		||||
            promises.push(AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, this.userId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(Promise.all(promises));
 | 
			
		||||
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open the page to view an attempt.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attempt Attempt.
 | 
			
		||||
     */
 | 
			
		||||
    openAttempt(attempt: AddonModH5PActivityAttempt): void {
 | 
			
		||||
        CoreNavigator.navigate(`../../attemptresults/${attempt.id}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										228
									
								
								src/addons/mod/h5pactivity/services/h5pactivity-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								src/addons/mod/h5pactivity/services/h5pactivity-sync.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,228 @@
 | 
			
		||||
// (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 { CoreNetworkError } from '@classes/errors/network-error';
 | 
			
		||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreXAPIOffline } from '@features/xapi/services/offline';
 | 
			
		||||
import { CoreXAPI } from '@features/xapi/services/xapi';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { AddonModH5PActivity, AddonModH5PActivityProvider } from './h5pactivity';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service to sync H5P activities.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModH5PActivitySyncResult> {
 | 
			
		||||
 | 
			
		||||
    static readonly AUTO_SYNCED = 'addon_mod_h5pactivity_autom_synced';
 | 
			
		||||
 | 
			
		||||
    protected componentTranslate?: string;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonModH5PActivitySyncProvider');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get component name translated.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Component name translated.
 | 
			
		||||
     */
 | 
			
		||||
    protected getComponentTranslate(): string {
 | 
			
		||||
        if (!this.componentTranslate) {
 | 
			
		||||
            this.componentTranslate = CoreCourse.translateModuleName('h5pactivity');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.componentTranslate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to synchronize all the H5P activities in a certain site or in all sites.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId Site ID to sync. If not defined, sync all sites.
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    syncAllActivities(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        return this.syncOnSites('H5P activities', this.syncAllActivitiesFunc.bind(this, !!force), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync all H5P activities on a site.
 | 
			
		||||
     *
 | 
			
		||||
     * @param force Wether to force sync not depending on last execution.
 | 
			
		||||
     * @param siteId Site ID to sync. If not defined, sync all sites.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected if sync fails.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncAllActivitiesFunc(force: boolean, siteId?: string): Promise<void> {
 | 
			
		||||
        const entries = await CoreXAPIOffline.getAllStatements(siteId);
 | 
			
		||||
 | 
			
		||||
        // Sync all responses.
 | 
			
		||||
        const promises = entries.map(async (response) => {
 | 
			
		||||
            const result = await (force ? this.syncActivity(response.contextid, siteId) :
 | 
			
		||||
                this.syncActivityIfNeeded(response.contextid, siteId));
 | 
			
		||||
 | 
			
		||||
            if (result?.updated) {
 | 
			
		||||
                // Sync successful, send event.
 | 
			
		||||
                CoreEvents.trigger(AddonModH5PActivitySyncProvider.AUTO_SYNCED, {
 | 
			
		||||
                    contextId: response.contextid,
 | 
			
		||||
                    warnings: result.warnings,
 | 
			
		||||
                }, siteId);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync an H5P activity only if a certain time has passed since the last time.
 | 
			
		||||
     *
 | 
			
		||||
     * @param contextId Context ID of the activity.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the activity is synced or it doesn't need to be synced.
 | 
			
		||||
     */
 | 
			
		||||
    async syncActivityIfNeeded(contextId: number, siteId?: string): Promise<AddonModH5PActivitySyncResult | undefined> {
 | 
			
		||||
        const needed = await this.isSyncNeeded(contextId, siteId);
 | 
			
		||||
 | 
			
		||||
        if (needed) {
 | 
			
		||||
            return this.syncActivity(contextId, siteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Synchronize an H5P activity. If it's already being synced it will reuse the same promise.
 | 
			
		||||
     *
 | 
			
		||||
     * @param contextId Context ID of the activity.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    syncActivity(contextId: number, siteId?: string): Promise<AddonModH5PActivitySyncResult> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        if (!CoreApp.isOnline()) {
 | 
			
		||||
            // Cannot sync in offline.
 | 
			
		||||
            throw new CoreNetworkError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isSyncing(contextId, siteId)) {
 | 
			
		||||
            // There's already a sync ongoing for this discussion, return the promise.
 | 
			
		||||
            return this.getOngoingSync(contextId, siteId)!;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.addOngoingSync(contextId, this.syncActivityData(contextId, siteId), siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Synchronize an H5P activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param contextId Context ID of the activity.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved if sync is successful, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected async syncActivityData(contextId: number, siteId: string): Promise<AddonModH5PActivitySyncResult> {
 | 
			
		||||
 | 
			
		||||
        this.logger.debug(`Try to sync H5P activity with context ID '${contextId}'`);
 | 
			
		||||
 | 
			
		||||
        const result: AddonModH5PActivitySyncResult = {
 | 
			
		||||
            warnings: [],
 | 
			
		||||
            updated: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get all the statements stored for the activity.
 | 
			
		||||
        const entries = await CoreXAPIOffline.getContextStatements(contextId, siteId);
 | 
			
		||||
 | 
			
		||||
        if (!entries || !entries.length) {
 | 
			
		||||
            // Nothing to sync.
 | 
			
		||||
            await this.setSyncTime(contextId, siteId);
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get the activity instance.
 | 
			
		||||
        const courseId = entries[0].courseid!;
 | 
			
		||||
 | 
			
		||||
        const h5pActivity = await AddonModH5PActivity.getH5PActivityByContextId(courseId, contextId, { siteId });
 | 
			
		||||
 | 
			
		||||
        // Sync offline logs.
 | 
			
		||||
        await CoreUtils.ignoreErrors(
 | 
			
		||||
            CoreCourseLogHelper.syncActivity(AddonModH5PActivityProvider.COMPONENT, h5pActivity.id, siteId),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Send the statements in order.
 | 
			
		||||
        for (let i = 0; i < entries.length; i++) {
 | 
			
		||||
            const entry = entries[i];
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                await CoreXAPI.postStatementsOnline(entry.component, entry.statements, siteId);
 | 
			
		||||
 | 
			
		||||
                result.updated = true;
 | 
			
		||||
 | 
			
		||||
                await CoreXAPIOffline.deleteStatements(entry.id, siteId);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                if (!CoreUtils.isWebServiceError(error)) {
 | 
			
		||||
                    throw error;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // The WebService has thrown an error, this means that statements cannot be submitted. Delete them.
 | 
			
		||||
                result.updated = true;
 | 
			
		||||
 | 
			
		||||
                await CoreXAPIOffline.deleteStatements(entry.id, siteId);
 | 
			
		||||
 | 
			
		||||
                // Responses deleted, add a warning.
 | 
			
		||||
                result.warnings.push(Translate.instant('core.warningofflinedatadeleted', {
 | 
			
		||||
                    component: this.componentTranslate,
 | 
			
		||||
                    name: entry.extra,
 | 
			
		||||
                    error: CoreTextUtils.getErrorMessageFromError(error),
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (result.updated) {
 | 
			
		||||
            // Data has been sent to server, invalidate attempts.
 | 
			
		||||
            await CoreUtils.ignoreErrors(AddonModH5PActivity.invalidateUserAttempts(h5pActivity.id, undefined, siteId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sync finished, set sync time.
 | 
			
		||||
        await this.setSyncTime(contextId, siteId);
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModH5PActivitySync = makeSingleton(AddonModH5PActivitySyncProvider);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Sync result.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivitySyncResult = {
 | 
			
		||||
    updated: boolean;
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to AUTO_SYNC event.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityAutoSyncData = {
 | 
			
		||||
    contextId: number;
 | 
			
		||||
    warnings: string[];
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										858
									
								
								src/addons/mod/h5pactivity/services/h5pactivity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										858
									
								
								src/addons/mod/h5pactivity/services/h5pactivity.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,858 @@
 | 
			
		||||
// (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 { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
 | 
			
		||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
 | 
			
		||||
import { CoreH5P } from '@features/h5p/services/h5p';
 | 
			
		||||
import { CoreH5PDisplayOptions } from '@features/h5p/classes/core';
 | 
			
		||||
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons/index';
 | 
			
		||||
import { CoreWSError } from '@classes/errors/wserror';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { AddonModH5PActivityAutoSyncData, AddonModH5PActivitySyncProvider } from './h5pactivity-sync';
 | 
			
		||||
 | 
			
		||||
const ROOT_CACHE_KEY = 'mmaModH5PActivity:';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service that provides some features for H5P activity.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModH5PActivityProvider {
 | 
			
		||||
 | 
			
		||||
    static readonly COMPONENT = 'mmaModH5PActivity';
 | 
			
		||||
    static readonly TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Format an attempt's data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attempt Attempt to format.
 | 
			
		||||
     * @return Formatted attempt.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatAttempt(attempt: AddonModH5PActivityWSAttempt): AddonModH5PActivityAttempt {
 | 
			
		||||
        const formattedAttempt: AddonModH5PActivityAttempt = attempt;
 | 
			
		||||
 | 
			
		||||
        formattedAttempt.timecreated = attempt.timecreated * 1000; // Convert to milliseconds.
 | 
			
		||||
        formattedAttempt.timemodified = attempt.timemodified * 1000; // Convert to milliseconds.
 | 
			
		||||
        formattedAttempt.success = formattedAttempt.success ?? null;
 | 
			
		||||
 | 
			
		||||
        if (!attempt.duration) {
 | 
			
		||||
            formattedAttempt.durationReadable = '-';
 | 
			
		||||
            formattedAttempt.durationCompact = '-';
 | 
			
		||||
        } else {
 | 
			
		||||
            formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration);
 | 
			
		||||
            formattedAttempt.durationCompact = CoreTimeUtils.formatDurationShort(attempt.duration);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return formattedAttempt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Format attempt data and results.
 | 
			
		||||
     *
 | 
			
		||||
     * @param attempt Attempt and results to format.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatAttemptResults(attempt: AddonModH5PActivityWSAttemptResults): AddonModH5PActivityAttemptResults {
 | 
			
		||||
        const formattedAttempt: AddonModH5PActivityAttemptResults = this.formatAttempt(attempt);
 | 
			
		||||
 | 
			
		||||
        formattedAttempt.results = formattedAttempt.results?.map((result) => this.formatResult(result));
 | 
			
		||||
 | 
			
		||||
        return formattedAttempt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Format the attempts of a user.
 | 
			
		||||
     *
 | 
			
		||||
     * @param data Data to format.
 | 
			
		||||
     * @return Formatted data.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatUserAttempts(data: AddonModH5PActivityWSUserAttempts): AddonModH5PActivityUserAttempts {
 | 
			
		||||
        const formatted: AddonModH5PActivityUserAttempts = data;
 | 
			
		||||
 | 
			
		||||
        formatted.attempts = formatted.attempts.map((attempt) => this.formatAttempt(attempt));
 | 
			
		||||
 | 
			
		||||
        if (formatted.scored) {
 | 
			
		||||
            formatted.scored.attempts = formatted.scored.attempts.map((attempt) => this.formatAttempt(attempt));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return formatted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Format an attempt's result.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Result to format.
 | 
			
		||||
     */
 | 
			
		||||
    protected formatResult(result: AddonModH5PActivityWSResult): AddonModH5PActivityWSResult {
 | 
			
		||||
        result.timecreated = result.timecreated * 1000; // Convert to milliseconds.
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get cache key for access information WS calls.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id H5P activity ID.
 | 
			
		||||
     * @return Cache key.
 | 
			
		||||
     */
 | 
			
		||||
    protected getAccessInformationCacheKey(id: number): string {
 | 
			
		||||
        return ROOT_CACHE_KEY + 'accessInfo:' + id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get access information for a given H5P activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id H5P activity ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the data.
 | 
			
		||||
     */
 | 
			
		||||
    async getAccessInformation(id: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModH5PActivityAccessInfo> {
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
 | 
			
		||||
        const params: AddonModH5pactivityGetH5pactivityAccessInformationWSParams = {
 | 
			
		||||
            h5pactivityid: id,
 | 
			
		||||
        };
 | 
			
		||||
        const preSets: CoreSiteWSPreSets = {
 | 
			
		||||
            cacheKey: this.getAccessInformationCacheKey(id),
 | 
			
		||||
            updateFrequency: CoreSite.FREQUENCY_OFTEN,
 | 
			
		||||
            component: AddonModH5PActivityProvider.COMPONENT,
 | 
			
		||||
            componentId: options.cmId,
 | 
			
		||||
            ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get attempt results for all user attempts.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the results of the attempt.
 | 
			
		||||
     */
 | 
			
		||||
    async getAllAttemptsResults(
 | 
			
		||||
        id: number,
 | 
			
		||||
        options?: AddonModH5PActivityGetAttemptResultsOptions,
 | 
			
		||||
    ): Promise<AddonModH5PActivityAttemptsResults> {
 | 
			
		||||
 | 
			
		||||
        const userAttempts = await this.getUserAttempts(id, options);
 | 
			
		||||
 | 
			
		||||
        const attemptIds = userAttempts.attempts.map((attempt) => attempt.id);
 | 
			
		||||
 | 
			
		||||
        if (attemptIds.length) {
 | 
			
		||||
            // Get all the attempts with a single call.
 | 
			
		||||
            return this.getAttemptsResults(id, attemptIds, options);
 | 
			
		||||
        } else {
 | 
			
		||||
            // No attempts.
 | 
			
		||||
            return {
 | 
			
		||||
                activityid: id,
 | 
			
		||||
                attempts: [],
 | 
			
		||||
                warnings: [],
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get cache key for results WS calls.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Instance ID.
 | 
			
		||||
     * @param attemptsIds Attempts IDs.
 | 
			
		||||
     * @return Cache key.
 | 
			
		||||
     */
 | 
			
		||||
    protected getAttemptResultsCacheKey(id: number, attemptsIds: number[]): string {
 | 
			
		||||
        return this.getAttemptResultsCommonCacheKey(id) + ':' + JSON.stringify(attemptsIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get common cache key for results WS calls.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Instance ID.
 | 
			
		||||
     * @return Cache key.
 | 
			
		||||
     */
 | 
			
		||||
    protected getAttemptResultsCommonCacheKey(id: number): string {
 | 
			
		||||
        return ROOT_CACHE_KEY + 'results:' + id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get attempt results.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the results of the attempt.
 | 
			
		||||
     */
 | 
			
		||||
    async getAttemptResults(
 | 
			
		||||
        id: number,
 | 
			
		||||
        attemptId: number,
 | 
			
		||||
        options?: AddonModH5PActivityGetAttemptResultsOptions,
 | 
			
		||||
    ): Promise<AddonModH5PActivityAttemptResults> {
 | 
			
		||||
 | 
			
		||||
        options = options || {};
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
 | 
			
		||||
        const params: AddonModH5pactivityGetResultsWSParams = {
 | 
			
		||||
            h5pactivityid: id,
 | 
			
		||||
            attemptids: [attemptId],
 | 
			
		||||
        };
 | 
			
		||||
        const preSets: CoreSiteWSPreSets = {
 | 
			
		||||
            cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids!),
 | 
			
		||||
            updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
 | 
			
		||||
            component: AddonModH5PActivityProvider.COMPONENT,
 | 
			
		||||
            componentId: options.cmId,
 | 
			
		||||
            ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await site.read<AddonModH5pactivityGetResultsWSResponse>(
 | 
			
		||||
                'mod_h5pactivity_get_results',
 | 
			
		||||
                params,
 | 
			
		||||
                preSets,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (response.warnings?.[0]) {
 | 
			
		||||
                throw new CoreWSError(response.warnings[0]); // Cannot view attempt.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.formatAttemptResults(response.attempts[0]);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (CoreUtils.isWebServiceError(error)) {
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if the full list of results is cached. If so, get the results from there.
 | 
			
		||||
            const cacheOptions: AddonModH5PActivityGetAttemptResultsOptions = {
 | 
			
		||||
                ...options, // Include all the original options.
 | 
			
		||||
                readingStrategy: CoreSitesReadingStrategy.OnlyCache,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const attemptsResults = await AddonModH5PActivity.getAllAttemptsResults(id, cacheOptions);
 | 
			
		||||
 | 
			
		||||
            const attempt = attemptsResults.attempts.find((attempt) => attempt.id == attemptId);
 | 
			
		||||
 | 
			
		||||
            if (!attempt) {
 | 
			
		||||
                throw error;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return attempt;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get attempts results.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param attemptsIds Attempts IDs.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with all the attempts.
 | 
			
		||||
     */
 | 
			
		||||
    async getAttemptsResults(
 | 
			
		||||
        id: number,
 | 
			
		||||
        attemptsIds: number[],
 | 
			
		||||
        options?: AddonModH5PActivityGetAttemptResultsOptions,
 | 
			
		||||
    ): Promise<AddonModH5PActivityAttemptsResults> {
 | 
			
		||||
 | 
			
		||||
        options = options || {};
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
 | 
			
		||||
        const params: AddonModH5pactivityGetResultsWSParams = {
 | 
			
		||||
            h5pactivityid: id,
 | 
			
		||||
            attemptids: attemptsIds,
 | 
			
		||||
        };
 | 
			
		||||
        const preSets: CoreSiteWSPreSets = {
 | 
			
		||||
            cacheKey: this.getAttemptResultsCommonCacheKey(id),
 | 
			
		||||
            updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
 | 
			
		||||
            component: AddonModH5PActivityProvider.COMPONENT,
 | 
			
		||||
            componentId: options.cmId,
 | 
			
		||||
            ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const response = await site.read<AddonModH5pactivityGetResultsWSResponse>(
 | 
			
		||||
            'mod_h5pactivity_get_results',
 | 
			
		||||
            params,
 | 
			
		||||
            preSets,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        response.attempts = response.attempts.map((attempt) => this.formatAttemptResults(attempt));
 | 
			
		||||
 | 
			
		||||
        return response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get deployed file from an H5P activity instance.
 | 
			
		||||
     *
 | 
			
		||||
     * @param h5pActivity Activity instance.
 | 
			
		||||
     * @param options Options
 | 
			
		||||
     * @return Promise resolved with the file.
 | 
			
		||||
     */
 | 
			
		||||
    async getDeployedFile(
 | 
			
		||||
        h5pActivity: AddonModH5PActivityData,
 | 
			
		||||
        options?: AddonModH5PActivityGetDeployedFileOptions,
 | 
			
		||||
    ): Promise<CoreWSExternalFile> {
 | 
			
		||||
 | 
			
		||||
        if (h5pActivity.deployedfile) {
 | 
			
		||||
            // File already deployed and still valid, use this one.
 | 
			
		||||
            return h5pActivity.deployedfile;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!h5pActivity.package || !h5pActivity.package[0]) {
 | 
			
		||||
            // Shouldn't happen.
 | 
			
		||||
            throw new CoreError('No H5P package found.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        options = options || {};
 | 
			
		||||
 | 
			
		||||
        // Deploy the file in the server.
 | 
			
		||||
        return CoreH5P.getTrustedH5PFile(
 | 
			
		||||
            h5pActivity.package[0].fileurl,
 | 
			
		||||
            options.displayOptions,
 | 
			
		||||
            options.ignoreCache,
 | 
			
		||||
            options.siteId,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get cache key for H5P activity data WS calls.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @return Cache key.
 | 
			
		||||
     */
 | 
			
		||||
    protected getH5PActivityDataCacheKey(courseId: number): string {
 | 
			
		||||
        return ROOT_CACHE_KEY + 'h5pactivity:' + courseId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an H5P activity with key=value. If more than one is found, only the first will be returned.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param key Name of the property to check.
 | 
			
		||||
     * @param value Value to search.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the activity data.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getH5PActivityByField(
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        key: string,
 | 
			
		||||
        value: unknown,
 | 
			
		||||
        options: CoreSitesCommonWSOptions = {},
 | 
			
		||||
    ): Promise<AddonModH5PActivityData> {
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
 | 
			
		||||
        const params: AddonModH5pactivityGetByCoursesWSParams = {
 | 
			
		||||
            courseids: [courseId],
 | 
			
		||||
        };
 | 
			
		||||
        const preSets: CoreSiteWSPreSets = {
 | 
			
		||||
            cacheKey: this.getH5PActivityDataCacheKey(courseId),
 | 
			
		||||
            updateFrequency: CoreSite.FREQUENCY_RARELY,
 | 
			
		||||
            component: AddonModH5PActivityProvider.COMPONENT,
 | 
			
		||||
            ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const response = await site.read<AddonModH5pactivityGetByCoursesWSResponse>(
 | 
			
		||||
            'mod_h5pactivity_get_h5pactivities_by_courses',
 | 
			
		||||
            params,
 | 
			
		||||
            preSets,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const currentActivity = response.h5pactivities.find((h5pActivity) => h5pActivity[key] == value);
 | 
			
		||||
 | 
			
		||||
        if (currentActivity) {
 | 
			
		||||
            return currentActivity;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new CoreError(Translate.instant('addon.mod_h5pactivity.errorgetactivity'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an H5P activity by module ID.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param cmId Course module ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the activity data.
 | 
			
		||||
     */
 | 
			
		||||
    getH5PActivity(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModH5PActivityData> {
 | 
			
		||||
        return this.getH5PActivityByField(courseId, 'coursemodule', cmId, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an H5P activity by context ID.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param contextId Context ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the activity data.
 | 
			
		||||
     */
 | 
			
		||||
    getH5PActivityByContextId(
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        contextId: number,
 | 
			
		||||
        options: CoreSitesCommonWSOptions = {},
 | 
			
		||||
    ): Promise<AddonModH5PActivityData> {
 | 
			
		||||
        return this.getH5PActivityByField(courseId, 'context', contextId, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an H5P activity by instance ID.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param id Instance ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the activity data.
 | 
			
		||||
     */
 | 
			
		||||
    getH5PActivityById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModH5PActivityData> {
 | 
			
		||||
        return this.getH5PActivityByField(courseId, 'id', id, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get cache key for attemps WS calls.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Instance ID.
 | 
			
		||||
     * @param userIds User IDs.
 | 
			
		||||
     * @return Cache key.
 | 
			
		||||
     */
 | 
			
		||||
    protected getUserAttemptsCacheKey(id: number, userIds: number[]): string {
 | 
			
		||||
        return this.getUserAttemptsCommonCacheKey(id) + ':' + JSON.stringify(userIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get common cache key for attempts WS calls.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Instance ID.
 | 
			
		||||
     * @return Cache key.
 | 
			
		||||
     */
 | 
			
		||||
    protected getUserAttemptsCommonCacheKey(id: number): string {
 | 
			
		||||
        return ROOT_CACHE_KEY + 'attempts:' + id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get attempts of a certain user.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with the attempts of the user.
 | 
			
		||||
     */
 | 
			
		||||
    async getUserAttempts(
 | 
			
		||||
        id: number,
 | 
			
		||||
        options: AddonModH5PActivityGetAttemptsOptions = {},
 | 
			
		||||
    ): Promise<AddonModH5PActivityUserAttempts> {
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
 | 
			
		||||
        const params: AddonModH5pactivityGetAttemptsWSParams = {
 | 
			
		||||
            h5pactivityid: id,
 | 
			
		||||
            userids: [options.userId || site.getUserId()],
 | 
			
		||||
        };
 | 
			
		||||
        const preSets: CoreSiteWSPreSets = {
 | 
			
		||||
            cacheKey: this.getUserAttemptsCacheKey(id, params.userids!),
 | 
			
		||||
            updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
 | 
			
		||||
            component: AddonModH5PActivityProvider.COMPONENT,
 | 
			
		||||
            componentId: options.cmId,
 | 
			
		||||
            ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const response = await site.read<AddonModH5pactivityGetAttemptsWSResponse>('mod_h5pactivity_get_attempts', params, preSets);
 | 
			
		||||
 | 
			
		||||
        if (response.warnings?.[0]) {
 | 
			
		||||
            throw new CoreWSError(response.warnings[0]); // Cannot view user attempts.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.formatUserAttempts(response.usersattempts[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates access information.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id H5P activity ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the data is invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateAccessInformation(id: number, siteId?: string): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates H5P activity data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the data is invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateActivityData(courseId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.invalidateWsCacheForKey(this.getH5PActivityDataCacheKey(courseId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates all attempts results for H5P activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the data is invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateAllResults(id: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.invalidateWsCacheForKey(this.getAttemptResultsCommonCacheKey(id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates results of a certain attempt for H5P activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the data is invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateAttemptResults(id: number, attemptId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.invalidateWsCacheForKey(this.getAttemptResultsCacheKey(id, [attemptId]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates all users attempts for H5P activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the data is invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateAllUserAttempts(id: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.invalidateWsCacheForKey(this.getUserAttemptsCommonCacheKey(id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates attempts of a certain user for H5P activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param userId User ID. If not defined, current user in the site.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the data is invalidated.
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateUserAttempts(id: number, userId?: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        userId = userId || site.getUserId();
 | 
			
		||||
 | 
			
		||||
        await site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(id, [userId]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete launcher.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when the launcher file is deleted.
 | 
			
		||||
     */
 | 
			
		||||
    async isPluginEnabled(siteId?: string): Promise<boolean> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        return site.wsAvailable('mod_h5pactivity_get_h5pactivities_by_courses');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Report an H5P activity as being viewed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id H5P activity ID.
 | 
			
		||||
     * @param name Name of the activity.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when the WS call is successful.
 | 
			
		||||
     */
 | 
			
		||||
    logView(id: number, name?: string, siteId?: string): Promise<void> {
 | 
			
		||||
        const params: AddonModH5pactivityViewH5pactivityWSParams = {
 | 
			
		||||
            h5pactivityid: id,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return CoreCourseLogHelper.logSingle(
 | 
			
		||||
            'mod_h5pactivity_view_h5pactivity',
 | 
			
		||||
            params,
 | 
			
		||||
            AddonModH5PActivityProvider.COMPONENT,
 | 
			
		||||
            id,
 | 
			
		||||
            name,
 | 
			
		||||
            'h5pactivity',
 | 
			
		||||
            {},
 | 
			
		||||
            siteId,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModH5PActivity = makeSingleton(AddonModH5PActivityProvider);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Basic data for an H5P activity, exported by Moodle class h5pactivity_summary_exporter.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityData = {
 | 
			
		||||
    id: number; // The primary key of the record.
 | 
			
		||||
    course: number; // Course id this h5p activity is part of.
 | 
			
		||||
    name: string; // The name of the activity module instance.
 | 
			
		||||
    timecreated?: number; // Timestamp of when the instance was added to the course.
 | 
			
		||||
    timemodified?: number; // Timestamp of when the instance was last modified.
 | 
			
		||||
    intro: string; // H5P activity description.
 | 
			
		||||
    introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
 | 
			
		||||
    grade?: number; // The maximum grade for submission.
 | 
			
		||||
    displayoptions: number; // H5P Button display options.
 | 
			
		||||
    enabletracking: number; // Enable xAPI tracking.
 | 
			
		||||
    grademethod: number; // Which H5P attempt is used for grading.
 | 
			
		||||
    contenthash?: string; // Sha1 hash of file content.
 | 
			
		||||
    coursemodule: number; // Coursemodule.
 | 
			
		||||
    context: number; // Context ID.
 | 
			
		||||
    introfiles: CoreWSExternalFile[];
 | 
			
		||||
    package: CoreWSExternalFile[];
 | 
			
		||||
    deployedfile?: {
 | 
			
		||||
        filename?: string; // File name.
 | 
			
		||||
        filepath?: string; // File path.
 | 
			
		||||
        filesize?: number; // File size.
 | 
			
		||||
        fileurl: string; // Downloadable file url.
 | 
			
		||||
        timemodified?: number; // Time modified.
 | 
			
		||||
        mimetype?: string; // File mime type.
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Params of mod_h5pactivity_get_h5pactivities_by_courses WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetByCoursesWSParams = {
 | 
			
		||||
    courseids?: number[]; // Array of course ids.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by mod_h5pactivity_get_h5pactivities_by_courses WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetByCoursesWSResponse = {
 | 
			
		||||
    h5pactivities: AddonModH5PActivityData[];
 | 
			
		||||
    warnings?: CoreWSExternalWarning[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Params of mod_h5pactivity_get_h5pactivity_access_information WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetH5pactivityAccessInformationWSParams = {
 | 
			
		||||
    h5pactivityid: number; // H5p activity instance id.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by mod_h5pactivity_get_h5pactivity_access_information WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetH5pactivityAccessInformationWSResponse = {
 | 
			
		||||
    warnings?: CoreWSExternalWarning[];
 | 
			
		||||
    canview?: boolean; // Whether the user has the capability mod/h5pactivity:view allowed.
 | 
			
		||||
    canaddinstance?: boolean; // Whether the user has the capability mod/h5pactivity:addinstance allowed.
 | 
			
		||||
    cansubmit?: boolean; // Whether the user has the capability mod/h5pactivity:submit allowed.
 | 
			
		||||
    canreviewattempts?: boolean; // Whether the user has the capability mod/h5pactivity:reviewattempts allowed.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Result of WS mod_h5pactivity_get_h5pactivity_access_information.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityAccessInfo = AddonModH5pactivityGetH5pactivityAccessInformationWSResponse;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Params of mod_h5pactivity_get_attempts WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetAttemptsWSParams = {
 | 
			
		||||
    h5pactivityid: number; // H5p activity instance id.
 | 
			
		||||
    userids?: number[]; // User ids.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by mod_h5pactivity_get_attempts WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetAttemptsWSResponse = {
 | 
			
		||||
    activityid: number; // Activity course module ID.
 | 
			
		||||
    usersattempts: AddonModH5PActivityWSUserAttempts[]; // The complete users attempts list.
 | 
			
		||||
    warnings?: CoreWSExternalWarning[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Params of mod_h5pactivity_get_results WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetResultsWSParams = {
 | 
			
		||||
    h5pactivityid: number; // H5p activity instance id.
 | 
			
		||||
    attemptids?: number[]; // Attempt ids.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data returned by mod_h5pactivity_get_results WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityGetResultsWSResponse = {
 | 
			
		||||
    activityid: number; // Activity course module ID.
 | 
			
		||||
    attempts: AddonModH5PActivityWSAttemptResults[]; // The complete attempts list.
 | 
			
		||||
    warnings?: CoreWSExternalWarning[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempts results with some calculated data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityAttemptsResults = Omit<AddonModH5pactivityGetResultsWSResponse, 'attempts'> & {
 | 
			
		||||
    attempts: AddonModH5PActivityAttemptResults[]; // The complete attempts list.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempts data for a user as returned by the WS mod_h5pactivity_get_attempts.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityWSUserAttempts = {
 | 
			
		||||
    userid: number; // The user id.
 | 
			
		||||
    attempts: AddonModH5PActivityWSAttempt[]; // The complete attempts list.
 | 
			
		||||
    scored?: { // Attempts used to grade the activity.
 | 
			
		||||
        title: string; // Scored attempts title.
 | 
			
		||||
        grademethod: string; // Scored attempts title.
 | 
			
		||||
        attempts: AddonModH5PActivityWSAttempt[]; // List of the grading attempts.
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempt data as returned by the WS mod_h5pactivity_get_attempts.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityWSAttempt = {
 | 
			
		||||
    id: number; // ID of the context.
 | 
			
		||||
    h5pactivityid: number; // ID of the H5P activity.
 | 
			
		||||
    userid: number; // ID of the user.
 | 
			
		||||
    timecreated: number; // Attempt creation.
 | 
			
		||||
    timemodified: number; // Attempt modified.
 | 
			
		||||
    attempt: number; // Attempt number.
 | 
			
		||||
    rawscore: number; // Attempt score value.
 | 
			
		||||
    maxscore: number; // Attempt max score.
 | 
			
		||||
    duration: number; // Attempt duration in seconds.
 | 
			
		||||
    completion?: number; // Attempt completion.
 | 
			
		||||
    success?: number | null; // Attempt success.
 | 
			
		||||
    scaled: number; // Attempt scaled.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempt and results data as returned by the WS mod_h5pactivity_get_results.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityWSAttemptResults = AddonModH5PActivityWSAttempt & {
 | 
			
		||||
    results?: AddonModH5PActivityWSResult[]; // The results of the attempt.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempt result data as returned by the WS mod_h5pactivity_get_results.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityWSResult = {
 | 
			
		||||
    id: number; // ID of the context.
 | 
			
		||||
    attemptid: number; // ID of the H5P attempt.
 | 
			
		||||
    subcontent: string; // Subcontent identifier.
 | 
			
		||||
    timecreated: number; // Result creation.
 | 
			
		||||
    interactiontype: string; // Interaction type.
 | 
			
		||||
    description: string; // Result description.
 | 
			
		||||
    content?: string; // Result extra content.
 | 
			
		||||
    rawscore: number; // Result score value.
 | 
			
		||||
    maxscore: number; // Result max score.
 | 
			
		||||
    duration?: number; // Result duration in seconds.
 | 
			
		||||
    completion?: number; // Result completion.
 | 
			
		||||
    success?: number | null; // Result success.
 | 
			
		||||
    optionslabel?: string; // Label used for result options.
 | 
			
		||||
    correctlabel?: string; // Label used for correct answers.
 | 
			
		||||
    answerlabel?: string; // Label used for user answers.
 | 
			
		||||
    track?: boolean; // If the result has valid track information.
 | 
			
		||||
    options?: { // The statement options.
 | 
			
		||||
        description: string; // Option description.
 | 
			
		||||
        id: number; // Option identifier.
 | 
			
		||||
        correctanswer: AddonModH5PActivityWSResultAnswer; // The option correct answer.
 | 
			
		||||
        useranswer: AddonModH5PActivityWSResultAnswer; // The option user answer.
 | 
			
		||||
    }[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Result answer as returned by the WS mod_h5pactivity_get_results.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityWSResultAnswer = {
 | 
			
		||||
    answer?: string; // Option text value.
 | 
			
		||||
    correct?: boolean; // If has to be displayed as correct.
 | 
			
		||||
    incorrect?: boolean; // If has to be displayed as incorrect.
 | 
			
		||||
    text?: boolean; // If has to be displayed as simple text.
 | 
			
		||||
    checked?: boolean; // If has to be displayed as a checked option.
 | 
			
		||||
    unchecked?: boolean; // If has to be displayed as a unchecked option.
 | 
			
		||||
    pass?: boolean; // If has to be displayed as passed.
 | 
			
		||||
    fail?: boolean; // If has to be displayed as failed.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * User attempts data with some calculated data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityUserAttempts = Omit<AddonModH5PActivityWSUserAttempts, 'attempts|scored'> & {
 | 
			
		||||
    attempts: AddonModH5PActivityAttempt[]; // The complete attempts list.
 | 
			
		||||
    scored?: { // Attempts used to grade the activity.
 | 
			
		||||
        title: string; // Scored attempts title.
 | 
			
		||||
        grademethod: string; // Scored attempts title.
 | 
			
		||||
        attempts: AddonModH5PActivityAttempt[]; // List of the grading attempts.
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempt with some calculated data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityAttempt = AddonModH5PActivityWSAttempt & {
 | 
			
		||||
    durationReadable?: string; // Duration in a human readable format.
 | 
			
		||||
    durationCompact?: string; // Duration in a "short" human readable format.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempt and results data with some calculated data.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityAttemptResults = AddonModH5PActivityAttempt & {
 | 
			
		||||
    results?: AddonModH5PActivityWSResult[]; // The results of the attempt.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options to pass to getDeployedFile function.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityGetDeployedFileOptions = {
 | 
			
		||||
    displayOptions?: CoreH5PDisplayOptions; // Display options
 | 
			
		||||
    ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down.
 | 
			
		||||
    siteId?: string; // Site ID. If not defined, current site.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options to pass to getAttemptResults function.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityGetAttemptResultsOptions = CoreCourseCommonModWSOptions & {
 | 
			
		||||
    userId?: number; // User ID. If not defined, user of the site.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options to pass to getAttempts function.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5PActivityGetAttemptsOptions = AddonModH5PActivityGetAttemptResultsOptions;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Params of mod_h5pactivity_view_h5pactivity WS.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonModH5pactivityViewH5pactivityWSParams = {
 | 
			
		||||
    h5pactivityid: number; // H5P activity instance id.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
declare module '@singletons/events' {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Augment CoreEventsData interface with events specific to this service.
 | 
			
		||||
     *
 | 
			
		||||
     * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
 | 
			
		||||
     */
 | 
			
		||||
    export interface CoreEventsData {
 | 
			
		||||
        [AddonModH5PActivitySyncProvider.AUTO_SYNCED]: AddonModH5PActivityAutoSyncData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								src/addons/mod/h5pactivity/services/handlers/index-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/addons/mod/h5pactivity/services/handlers/index-link.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to treat links to H5P activity index.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModH5PActivityIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModH5PActivityIndexLinkHandler';
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('AddonModH5PActivity', 'h5pactivity');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModH5PActivityIndexLinkHandler = makeSingleton(AddonModH5PActivityIndexLinkHandlerService);
 | 
			
		||||
							
								
								
									
										85
									
								
								src/addons/mod/h5pactivity/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/addons/mod/h5pactivity/services/handlers/module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
// (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 { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { Injectable, Type } from '@angular/core';
 | 
			
		||||
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseModule } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModH5PActivityIndexComponent } from '../../components/index';
 | 
			
		||||
import { AddonModH5PActivity } from '../h5pactivity';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to support H5P activities.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModH5PActivityModuleHandlerService implements CoreCourseModuleHandler {
 | 
			
		||||
 | 
			
		||||
    static readonly PAGE_NAME = 'mod_h5pactivity';
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModH5PActivity';
 | 
			
		||||
    modName = 'h5pactivity';
 | 
			
		||||
 | 
			
		||||
    supportedFeatures = {
 | 
			
		||||
        [CoreConstants.FEATURE_GROUPS]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_GROUPINGS]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_MOD_INTRO]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_MODEDIT_DEFAULT_COMPLETION]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
 | 
			
		||||
        [CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled(): Promise<boolean> {
 | 
			
		||||
        return AddonModH5PActivity.isPluginEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
 | 
			
		||||
            title: module.name,
 | 
			
		||||
            class: 'addon-mod_h5pactivity-handler',
 | 
			
		||||
            showDownloadButton: true,
 | 
			
		||||
            action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) {
 | 
			
		||||
                options = options || {};
 | 
			
		||||
                options.params = options.params || {};
 | 
			
		||||
                Object.assign(options.params, { module });
 | 
			
		||||
                const routeParams = '/' + courseId + '/' + module.id;
 | 
			
		||||
 | 
			
		||||
                CoreNavigator.navigateToSitePath(AddonModH5PActivityModuleHandlerService.PAGE_NAME + routeParams, options);
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getMainComponent(): Promise<Type<unknown>> {
 | 
			
		||||
        return AddonModH5PActivityIndexComponent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModH5PActivityModuleHandler = makeSingleton(AddonModH5PActivityModuleHandlerService);
 | 
			
		||||
							
								
								
									
										171
									
								
								src/addons/mod/h5pactivity/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								src/addons/mod/h5pactivity/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,171 @@
 | 
			
		||||
// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
 | 
			
		||||
import { CoreCourseAnyModuleData } from '@features/course/services/course';
 | 
			
		||||
import { CoreH5PHelper } from '@features/h5p/classes/helper';
 | 
			
		||||
import { CoreH5P } from '@features/h5p/services/h5p';
 | 
			
		||||
import { CoreUser } from '@features/user/services/user';
 | 
			
		||||
import { CoreFilepool } from '@services/filepool';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreWSExternalFile } from '@services/ws';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModH5PActivity, AddonModH5PActivityData, AddonModH5PActivityProvider } from '../h5pactivity';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to prefetch h5p activity.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModH5PActivity';
 | 
			
		||||
    modName = 'h5pactivity';
 | 
			
		||||
    component = AddonModH5PActivityProvider.COMPONENT;
 | 
			
		||||
    updatesNames = /^configuration$|^.*files$|^tracks$|^usertracks$/;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
 | 
			
		||||
 | 
			
		||||
        const h5pActivity = await AddonModH5PActivity.getH5PActivity(courseId, module.id);
 | 
			
		||||
 | 
			
		||||
        const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions);
 | 
			
		||||
 | 
			
		||||
        const deployedFile = await AddonModH5PActivity.getDeployedFile(h5pActivity, {
 | 
			
		||||
            displayOptions,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return [deployedFile].concat(this.getIntroFilesFromInstance(module, h5pActivity));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async invalidateModule(): Promise<void> {
 | 
			
		||||
        // No need to invalidate anything.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async isDownloadable(): Promise<boolean> {
 | 
			
		||||
        return !!CoreSites.getCurrentSite()?.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled(): Promise<boolean> {
 | 
			
		||||
        return AddonModH5PActivity.isPluginEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
 | 
			
		||||
        return this.prefetchPackage(module, courseId, this.prefetchActivity.bind(this, module, courseId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch an H5P activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID the module belongs to.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchActivity(
 | 
			
		||||
        module: CoreCourseAnyModuleData,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        siteId?: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        siteId = siteId || CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        const h5pActivity = await AddonModH5PActivity.getH5PActivity(courseId, module.id, {
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const introFiles = this.getIntroFilesFromInstance(module, h5pActivity);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            this.prefetchWSData(h5pActivity, siteId),
 | 
			
		||||
            CoreFilepool.addFilesToQueue(siteId, introFiles, AddonModH5PActivityProvider.COMPONENT, module.id),
 | 
			
		||||
            this.prefetchMainFile(module, h5pActivity, siteId),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch the deployed file of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param h5pActivity Activity instance.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchMainFile(
 | 
			
		||||
        module: CoreCourseAnyModuleData,
 | 
			
		||||
        h5pActivity: AddonModH5PActivityData,
 | 
			
		||||
        siteId: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions);
 | 
			
		||||
 | 
			
		||||
        const deployedFile = await AddonModH5PActivity.getDeployedFile(h5pActivity, {
 | 
			
		||||
            displayOptions: displayOptions,
 | 
			
		||||
            ignoreCache: true,
 | 
			
		||||
            siteId: siteId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await CoreFilepool.addFilesToQueue(siteId, [deployedFile], AddonModH5PActivityProvider.COMPONENT, module.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Prefetch all the WebService data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param h5pActivity Activity instance.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async prefetchWSData(h5pActivity: AddonModH5PActivityData, siteId: string): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const accessInfo = await AddonModH5PActivity.getAccessInformation(h5pActivity.id, {
 | 
			
		||||
            cmId: h5pActivity.coursemodule,
 | 
			
		||||
            readingStrategy: CoreSitesReadingStrategy.PreferCache,
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!accessInfo.canreviewattempts) {
 | 
			
		||||
            // Not a teacher, prefetch user attempts and the current user profile.
 | 
			
		||||
            const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
            const options = {
 | 
			
		||||
                cmId: h5pActivity.coursemodule,
 | 
			
		||||
                readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
			
		||||
                siteId: siteId,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                AddonModH5PActivity.getAllAttemptsResults(h5pActivity.id, options),
 | 
			
		||||
                CoreUser.prefetchProfiles([site.getUserId()], h5pActivity.course, siteId),
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModH5PActivityPrefetchHandler = makeSingleton(AddonModH5PActivityPrefetchHandlerService);
 | 
			
		||||
							
								
								
									
										136
									
								
								src/addons/mod/h5pactivity/services/handlers/report-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/addons/mod/h5pactivity/services/handlers/report-link.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,136 @@
 | 
			
		||||
// (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 { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
 | 
			
		||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModH5PActivity } from '../h5pactivity';
 | 
			
		||||
import { AddonModH5PActivityModuleHandlerService } from './module';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to treat links to H5P activity report.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModH5PActivityReportLinkHandlerService extends CoreContentLinksHandlerBase {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModH5PActivityReportLinkHandler';
 | 
			
		||||
    featureName = 'CoreCourseModuleDelegate_AddonModH5PActivity';
 | 
			
		||||
    pattern = /\/mod\/h5pactivity\/report\.php.*([&?]a=\d+)/;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    getActions(
 | 
			
		||||
        siteIds: string[],
 | 
			
		||||
        url: string,
 | 
			
		||||
        params: Record<string, string>,
 | 
			
		||||
        courseId?: number,
 | 
			
		||||
    ): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
 | 
			
		||||
        courseId = courseId || Number(params.courseid) || Number(params.cid);
 | 
			
		||||
 | 
			
		||||
        return [{
 | 
			
		||||
            action: async (siteId) => {
 | 
			
		||||
                try {
 | 
			
		||||
                    const instanceId = Number(params.a);
 | 
			
		||||
 | 
			
		||||
                    if (!courseId) {
 | 
			
		||||
                        courseId = await this.getCourseId(instanceId, siteId);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const module = await CoreCourse.getModuleBasicInfoByInstance(instanceId, 'h5pactivity', siteId);
 | 
			
		||||
 | 
			
		||||
                    if (typeof params.attemptid != 'undefined') {
 | 
			
		||||
                        this.openAttemptResults(module.id, Number(params.attemptid), courseId, siteId);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        const userId = params.userid ? Number(params.userid) : undefined;
 | 
			
		||||
 | 
			
		||||
                        this.openUserAttempts(module.id, courseId, siteId, userId);
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    CoreDomUtils.showErrorModalDefault(error, 'Error processing link.');
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get course Id for an activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Activity ID.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved with course ID.
 | 
			
		||||
     */
 | 
			
		||||
    protected async getCourseId(id: number, siteId: string): Promise<number> {
 | 
			
		||||
        const modal = await CoreDomUtils.showModalLoading();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const module = await CoreCourse.getModuleBasicInfoByInstance(id, 'h5pactivity', siteId);
 | 
			
		||||
 | 
			
		||||
            return module.course;
 | 
			
		||||
        } finally {
 | 
			
		||||
            modal.dismiss();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled(): Promise<boolean> {
 | 
			
		||||
        return AddonModH5PActivity.isPluginEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open attempt results.
 | 
			
		||||
     *
 | 
			
		||||
     * @param cmId Module ID.
 | 
			
		||||
     * @param attemptId Attempt ID.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected openAttemptResults(cmId: number, attemptId: number, courseId: number, siteId: string): void {
 | 
			
		||||
        const path = AddonModH5PActivityModuleHandlerService.PAGE_NAME + `/${courseId}/${cmId}/attemptresults/${attemptId}`;
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigateToSitePath(path, {
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open user attempts.
 | 
			
		||||
     *
 | 
			
		||||
     * @param cmId Module ID.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @param userId User ID. If not defined, current user in site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected openUserAttempts(cmId: number, courseId: number, siteId: string, userId?: number): void {
 | 
			
		||||
        userId = userId || CoreSites.getCurrentSiteUserId();
 | 
			
		||||
        const path = AddonModH5PActivityModuleHandlerService.PAGE_NAME + `/${courseId}/${cmId}/userattempts/${userId}`;
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigateToSitePath(path, {
 | 
			
		||||
            siteId,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModH5PActivityReportLinkHandler = makeSingleton(AddonModH5PActivityReportLinkHandlerService);
 | 
			
		||||
							
								
								
									
										52
									
								
								src/addons/mod/h5pactivity/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/addons/mod/h5pactivity/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
// (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 { CoreCronHandler } from '@services/cron';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModH5PActivitySync } from '../h5pactivity-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Synchronization cron handler.
 | 
			
		||||
 */
 | 
			
		||||
@Injectable({ providedIn: 'root' })
 | 
			
		||||
export class AddonModH5PActivitySyncCronHandlerService implements CoreCronHandler {
 | 
			
		||||
 | 
			
		||||
    name = 'AddonModH5PActivitySyncCronHandler';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the process.
 | 
			
		||||
     * Receives the ID of the site affected, undefined for all sites.
 | 
			
		||||
     *
 | 
			
		||||
     * @param siteId ID of the site affected, undefined for all sites.
 | 
			
		||||
     * @param force Wether the execution is forced (manual sync).
 | 
			
		||||
     * @return Promise resolved when done, rejected if failure.
 | 
			
		||||
     */
 | 
			
		||||
    execute(siteId?: string, force?: boolean): Promise<void> {
 | 
			
		||||
        return AddonModH5PActivitySync.syncAllActivities(siteId, force);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the time between consecutive executions.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Time between consecutive executions (in ms).
 | 
			
		||||
     */
 | 
			
		||||
    getInterval(): number {
 | 
			
		||||
        return AddonModH5PActivitySync.syncInterval;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AddonModH5PActivitySyncCronHandler = makeSingleton(AddonModH5PActivitySyncCronHandlerService);
 | 
			
		||||
@ -26,6 +26,7 @@ import { AddonModQuizModule } from './quiz/quiz.module';
 | 
			
		||||
import { AddonModResourceModule } from './resource/resource.module';
 | 
			
		||||
import { AddonModUrlModule } from './url/url.module';
 | 
			
		||||
import { AddonModLtiModule } from './lti/lti.module';
 | 
			
		||||
import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [],
 | 
			
		||||
@ -42,6 +43,7 @@ import { AddonModLtiModule } from './lti/lti.module';
 | 
			
		||||
        AddonModFolderModule,
 | 
			
		||||
        AddonModImscpModule,
 | 
			
		||||
        AddonModLtiModule,
 | 
			
		||||
        AddonModH5PActivityModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [],
 | 
			
		||||
    exports: [],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								src/assets/img/icons/empty.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/assets/img/icons/empty.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
 | 
			
		||||
	<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
 | 
			
		||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 299 B  | 
							
								
								
									
										42
									
								
								src/assets/js/iframe-recaptcha.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/assets/js/iframe-recaptcha.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
// (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.
 | 
			
		||||
 | 
			
		||||
(function () {
 | 
			
		||||
    var url = location.href;
 | 
			
		||||
 | 
			
		||||
    if (!url.match(/^https?:\/\//i) || !url.match(/\/webservice\/recaptcha\.php/i)) {
 | 
			
		||||
        // Not the recaptcha script, stop.
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Define recaptcha callbacks.
 | 
			
		||||
    window.recaptchacallback = function(value) {
 | 
			
		||||
        window.parent.postMessage({
 | 
			
		||||
            environment: 'moodleapp',
 | 
			
		||||
            context: 'recaptcha',
 | 
			
		||||
            action: 'callback',
 | 
			
		||||
            frameUrl: location.href,
 | 
			
		||||
            value: value,
 | 
			
		||||
        }, '*');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    window.recaptchaexpiredcallback = function() {
 | 
			
		||||
        window.parent.postMessage({
 | 
			
		||||
            environment: 'moodleapp',
 | 
			
		||||
            context: 'recaptcha',
 | 
			
		||||
            action: 'expired',
 | 
			
		||||
            frameUrl: location.href,
 | 
			
		||||
        }, '*');
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										210
									
								
								src/assets/js/iframe-treat-links.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/assets/js/iframe-treat-links.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,210 @@
 | 
			
		||||
// (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.
 | 
			
		||||
 | 
			
		||||
(function () {
 | 
			
		||||
    var url = location.href;
 | 
			
		||||
 | 
			
		||||
    if (url.match(/^moodleappfs:\/\/localhost/i) || !url.match(/^[a-z0-9]+:\/\//i)) {
 | 
			
		||||
        // Same domain as the app, stop.
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Redefine window.open.
 | 
			
		||||
    window.open = function(url, name, specs) {
 | 
			
		||||
        if (name == '_self') {
 | 
			
		||||
            // Link should be loaded in the same frame.
 | 
			
		||||
            location.href = toAbsolute(url);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        getRootWindow(window).postMessage({
 | 
			
		||||
            environment: 'moodleapp',
 | 
			
		||||
            context: 'iframe',
 | 
			
		||||
            action: 'window_open',
 | 
			
		||||
            frameUrl: location.href,
 | 
			
		||||
            url: url,
 | 
			
		||||
            name: name,
 | 
			
		||||
            specs: specs,
 | 
			
		||||
        }, '*');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Handle link clicks.
 | 
			
		||||
    document.addEventListener('click', (event) => {
 | 
			
		||||
        if (event.defaultPrevented) {
 | 
			
		||||
            // Event already prevented by some other code.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Find the link being clicked.
 | 
			
		||||
        var el = event.target;
 | 
			
		||||
        while (el && (el.tagName !== 'A' && el.tagName !== 'a')) {
 | 
			
		||||
            el = el.parentElement;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!el || el.treated) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add click listener to the link, this way if the iframe has added a listener to the link it will be executed first.
 | 
			
		||||
        el.treated = true;
 | 
			
		||||
        el.addEventListener('click', function(event) {
 | 
			
		||||
            linkClicked(el, event);
 | 
			
		||||
        });
 | 
			
		||||
    }, {
 | 
			
		||||
        capture: true // Use capture to fix this listener not called if the element clicked is too deep in the DOM.
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Concatenate two paths, adding a slash between them if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param leftPath Left path.
 | 
			
		||||
     * @param rightPath Right path.
 | 
			
		||||
     * @return Concatenated path.
 | 
			
		||||
     */
 | 
			
		||||
    function concatenatePaths(leftPath, rightPath) {
 | 
			
		||||
        if (!leftPath) {
 | 
			
		||||
            return rightPath;
 | 
			
		||||
        } else if (!rightPath) {
 | 
			
		||||
            return leftPath;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var lastCharLeft = leftPath.slice(-1);
 | 
			
		||||
        var firstCharRight = rightPath.charAt(0);
 | 
			
		||||
 | 
			
		||||
        if (lastCharLeft === '/' && firstCharRight === '/') {
 | 
			
		||||
            return leftPath + rightPath.substr(1);
 | 
			
		||||
        } else if (lastCharLeft !== '/' && firstCharRight !== '/') {
 | 
			
		||||
            return leftPath + '/' + rightPath;
 | 
			
		||||
        } else {
 | 
			
		||||
            return leftPath + rightPath;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the root window.
 | 
			
		||||
     *
 | 
			
		||||
     * @param win Current window to check.
 | 
			
		||||
     * @return Root window.
 | 
			
		||||
     */
 | 
			
		||||
    function getRootWindow(win) {
 | 
			
		||||
        if (win.parent === win) {
 | 
			
		||||
            return win;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return getRootWindow(win.parent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the scheme from a URL.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url URL to treat.
 | 
			
		||||
     * @return Scheme, undefined if no scheme found.
 | 
			
		||||
     */
 | 
			
		||||
    function getUrlScheme(url) {
 | 
			
		||||
        if (!url) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var matches = url.match(/^([a-z][a-z0-9+\-.]*):/);
 | 
			
		||||
        if (matches && matches[1]) {
 | 
			
		||||
            return matches[1];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a URL is absolute.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url URL to treat.
 | 
			
		||||
     * @return Whether it's absolute.
 | 
			
		||||
     */
 | 
			
		||||
    function isAbsoluteUrl(url) {
 | 
			
		||||
        return /^[^:]{2,}:\/\//i.test(url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether a URL scheme belongs to a local file.
 | 
			
		||||
     *
 | 
			
		||||
     * @param scheme Scheme to check.
 | 
			
		||||
     * @return Whether the scheme belongs to a local file.
 | 
			
		||||
     */
 | 
			
		||||
    function isLocalFileUrlScheme(scheme) {
 | 
			
		||||
        if (scheme) {
 | 
			
		||||
            scheme = scheme.toLowerCase();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return scheme == 'cdvfile' ||
 | 
			
		||||
                scheme == 'file' ||
 | 
			
		||||
                scheme == 'filesystem' ||
 | 
			
		||||
                scheme == 'moodleappfs';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle a click on an anchor element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param link Anchor element clicked.
 | 
			
		||||
     * @param event Click event.
 | 
			
		||||
     */
 | 
			
		||||
    function linkClicked(link, event) {
 | 
			
		||||
        if (event.defaultPrevented) {
 | 
			
		||||
            // Event already prevented by some other code.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var linkScheme = getUrlScheme(link.href);
 | 
			
		||||
        var pageScheme = getUrlScheme(location.href);
 | 
			
		||||
        var isTargetSelf = !link.target || link.target == '_self';
 | 
			
		||||
 | 
			
		||||
        if (!link.href || linkScheme == 'javascript') {
 | 
			
		||||
            // Links with no URL and Javascript links are ignored.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
 | 
			
		||||
        if (isTargetSelf && (isLocalFileUrlScheme(linkScheme) || !isLocalFileUrlScheme(pageScheme))) {
 | 
			
		||||
            // Link should be loaded in the same frame. Don't do it if link is online and frame is local.
 | 
			
		||||
            location.href = toAbsolute(link.href);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        getRootWindow(window).postMessage({
 | 
			
		||||
            environment: 'moodleapp',
 | 
			
		||||
            context: 'iframe',
 | 
			
		||||
            action: 'link_clicked',
 | 
			
		||||
            frameUrl: location.href,
 | 
			
		||||
            link: {href: link.href, target: link.target},
 | 
			
		||||
        }, '*');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a URL to an absolute URL if needed using the frame src.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url URL to convert.
 | 
			
		||||
     * @return Absolute URL.
 | 
			
		||||
     */
 | 
			
		||||
    function toAbsolute(url) {
 | 
			
		||||
        if (isAbsoluteUrl(url)) {
 | 
			
		||||
            return url;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // It's a relative URL, use the frame src to create the full URL.
 | 
			
		||||
        var pathToDir = location.href.substring(0, location.href.lastIndexOf('/'));
 | 
			
		||||
 | 
			
		||||
        return concatenatePaths(pathToDir, url);
 | 
			
		||||
    }
 | 
			
		||||
})();
 | 
			
		||||
@ -482,6 +482,7 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
        const stopClicksElements = Array.from(div.querySelectorAll('button,input,select,textarea'));
 | 
			
		||||
        const frames = Array.from(div.querySelectorAll(CoreIframeUtilsProvider.FRAME_TAGS.join(',').replace(/iframe,?/, '')));
 | 
			
		||||
        const svgImages = Array.from(div.querySelectorAll('image'));
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        // Walk through the content to find the links and add our directive to it.
 | 
			
		||||
        // Important: We need to look for links first because in 'img' we add new links without core-link.
 | 
			
		||||
@ -520,7 +521,7 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        iframes.forEach((iframe) => {
 | 
			
		||||
            this.treatIframe(iframe, site, canTreatVimeo);
 | 
			
		||||
            promises.push(this.treatIframe(iframe, site, canTreatVimeo));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        svgImages.forEach((image) => {
 | 
			
		||||
@ -570,8 +571,10 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Automatically reject the promise after 5 seconds to prevent blocking the user forever.
 | 
			
		||||
            await CoreUtils.ignoreErrors(CoreUtils.timeoutPromise(promise, 5000));
 | 
			
		||||
            promises.push(CoreUtils.ignoreErrors(CoreUtils.timeoutPromise(promise, 5000)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -679,6 +682,7 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
        if (currentSite?.containsUrl(src)) {
 | 
			
		||||
            // URL points to current site, try to use auto-login.
 | 
			
		||||
            const finalUrl = await currentSite.getAutoLoginUrl(src, false);
 | 
			
		||||
            await CoreIframeUtils.fixIframeCookies(finalUrl);
 | 
			
		||||
 | 
			
		||||
            iframe.src = finalUrl;
 | 
			
		||||
            CoreIframeUtils.treatFrame(iframe, false);
 | 
			
		||||
@ -686,6 +690,8 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreIframeUtils.fixIframeCookies(src);
 | 
			
		||||
 | 
			
		||||
        if (site && src && canTreatVimeo) {
 | 
			
		||||
            // Check if it's a Vimeo video. If it is, use the wsplayer script instead to make restricted videos work.
 | 
			
		||||
            const matches = iframe.src.match(/https?:\/\/player\.vimeo\.com\/video\/([0-9]+)/);
 | 
			
		||||
@ -694,8 +700,8 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
                    matches[1] + '&token=' + site.getToken();
 | 
			
		||||
 | 
			
		||||
                // Width and height are mandatory, we need to calculate them.
 | 
			
		||||
                let width;
 | 
			
		||||
                let height;
 | 
			
		||||
                let width: string | number;
 | 
			
		||||
                let height: string | number;
 | 
			
		||||
 | 
			
		||||
                if (iframe.width) {
 | 
			
		||||
                    width = iframe.width;
 | 
			
		||||
@ -719,13 +725,16 @@ export class CoreFormatTextDirective implements OnChanges {
 | 
			
		||||
                if (site && !site.isVersionGreaterEqualThan('3.7')) {
 | 
			
		||||
                    newUrl += '&width=' + width + '&height=' + height;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await CoreIframeUtils.fixIframeCookies(src);
 | 
			
		||||
 | 
			
		||||
                iframe.src = newUrl;
 | 
			
		||||
 | 
			
		||||
                if (!iframe.width) {
 | 
			
		||||
                    iframe.width = width;
 | 
			
		||||
                    iframe.width = String(width);
 | 
			
		||||
                }
 | 
			
		||||
                if (!iframe.height) {
 | 
			
		||||
                    iframe.height = height;
 | 
			
		||||
                    iframe.height = String(height);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Do the iframe responsive.
 | 
			
		||||
 | 
			
		||||
@ -129,7 +129,7 @@ import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module';
 | 
			
		||||
import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module';
 | 
			
		||||
import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
 | 
			
		||||
// @todo import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module';
 | 
			
		||||
// @todo import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module';
 | 
			
		||||
import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module';
 | 
			
		||||
import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module';
 | 
			
		||||
import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module';
 | 
			
		||||
import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module';
 | 
			
		||||
@ -294,7 +294,7 @@ export class CoreCompileProvider {
 | 
			
		||||
            ...ADDON_MOD_FOLDER_SERVICES,
 | 
			
		||||
            ...ADDON_MOD_FORUM_SERVICES,
 | 
			
		||||
            // @todo ...ADDON_MOD_GLOSSARY_SERVICES,
 | 
			
		||||
            // @todo ...ADDON_MOD_H5P_ACTIVITY_SERVICES,
 | 
			
		||||
            ...ADDON_MOD_H5P_ACTIVITY_SERVICES,
 | 
			
		||||
            ...ADDON_MOD_IMSCP_SERVICES,
 | 
			
		||||
            ...ADDON_MOD_LESSON_SERVICES,
 | 
			
		||||
            ...ADDON_MOD_LTI_SERVICES,
 | 
			
		||||
 | 
			
		||||
@ -226,7 +226,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
 | 
			
		||||
    async gotoBlog(): Promise<void> {
 | 
			
		||||
        const params: Params = { cmId: this.module.id };
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
 | 
			
		||||
        await CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -1038,7 +1038,7 @@ export class CoreCourseProvider {
 | 
			
		||||
        const loading = await CoreDomUtils.showModalLoading();
 | 
			
		||||
 | 
			
		||||
        // Wait for site plugins to be fetched.
 | 
			
		||||
        await CoreSitePlugins.waitFetchPlugins();
 | 
			
		||||
        await CoreUtils.ignoreErrors(CoreSitePlugins.waitFetchPlugins());
 | 
			
		||||
 | 
			
		||||
        if (!('format' in course) || typeof course.format == 'undefined') {
 | 
			
		||||
            const result = await CoreCourseHelper.getCourse(course.id);
 | 
			
		||||
@ -1050,8 +1050,8 @@ export class CoreCourseProvider {
 | 
			
		||||
 | 
			
		||||
        if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) {
 | 
			
		||||
            // No custom format plugin. We don't need to wait for anything.
 | 
			
		||||
            await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params);
 | 
			
		||||
            loading.dismiss();
 | 
			
		||||
            await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -1084,6 +1084,8 @@ export class CoreCourseProvider {
 | 
			
		||||
 | 
			
		||||
            await CoreDomUtils.showConfirm(message, '', reload, ignore);
 | 
			
		||||
            window.location.reload();
 | 
			
		||||
        } finally {
 | 
			
		||||
            loading.dismiss();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -376,7 +376,7 @@ export class CoreH5PFileStorage {
 | 
			
		||||
     * @return Folder path.
 | 
			
		||||
     */
 | 
			
		||||
    getCoreH5PPath(): string {
 | 
			
		||||
        return CoreTextUtils.concatenatePaths(CoreFile.getWWWPath(), '/h5p/');
 | 
			
		||||
        return CoreTextUtils.concatenatePaths(CoreFile.getWWWPath(), '/assets/lib/h5p/');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -68,7 +68,6 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy {
 | 
			
		||||
        this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite();
 | 
			
		||||
 | 
			
		||||
        // Send resize events when the page holding this component is re-entered.
 | 
			
		||||
        // @todo: Check that this works as expected.
 | 
			
		||||
        this.currentPageRoute = router.url;
 | 
			
		||||
        this.subscription = router.events
 | 
			
		||||
            .pipe(filter(event => event instanceof NavigationEnd))
 | 
			
		||||
 | 
			
		||||
@ -431,7 +431,21 @@ export class CoreSitePluginsHelperProvider {
 | 
			
		||||
        styleEl.setAttribute('id', 'siteplugin-' + uniqueName);
 | 
			
		||||
        styleEl.innerHTML = cssCode;
 | 
			
		||||
 | 
			
		||||
        document.head.appendChild(styleEl);
 | 
			
		||||
        // To ensure consistency, insert in alphabetical order among other site plugin styles.
 | 
			
		||||
        let lowestGreater: HTMLStyleElement | null = null;
 | 
			
		||||
        Array.from(document.head.querySelectorAll('style')).forEach((other) => {
 | 
			
		||||
            if (/^siteplugin-/.test(other.id) && other.id > styleEl.id) {
 | 
			
		||||
                if (lowestGreater === null || other.id < lowestGreater.id) {
 | 
			
		||||
                    lowestGreater = other;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (lowestGreater) {
 | 
			
		||||
            document.head.insertBefore(styleEl, lowestGreater);
 | 
			
		||||
        } else {
 | 
			
		||||
            document.head.appendChild(styleEl);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Styles have been loaded, now treat the CSS.
 | 
			
		||||
        CoreUtils.ignoreErrors(
 | 
			
		||||
 | 
			
		||||
@ -211,11 +211,11 @@ export type SiteDBEntry = {
 | 
			
		||||
    id: string;
 | 
			
		||||
    siteUrl: string;
 | 
			
		||||
    token: string;
 | 
			
		||||
    info: string;
 | 
			
		||||
    info?: string | null;
 | 
			
		||||
    privateToken: string;
 | 
			
		||||
    config: string;
 | 
			
		||||
    config?: string | null;
 | 
			
		||||
    loggedOut: number;
 | 
			
		||||
    oauthId: number;
 | 
			
		||||
    oauthId?: number | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CurrentSiteDBEntry = {
 | 
			
		||||
 | 
			
		||||
@ -1215,7 +1215,12 @@ export class CoreFileProvider {
 | 
			
		||||
     * @return Path.
 | 
			
		||||
     */
 | 
			
		||||
    getWWWPath(): string {
 | 
			
		||||
        const position = window.location.href.indexOf('index.html');
 | 
			
		||||
        // Use current URL, removing the path.
 | 
			
		||||
        if (!window.location.pathname || window.location.pathname == '/') {
 | 
			
		||||
            return window.location.href;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = window.location.href.indexOf(window.location.pathname);
 | 
			
		||||
 | 
			
		||||
        if (position != -1) {
 | 
			
		||||
            return window.location.href.substr(0, position);
 | 
			
		||||
 | 
			
		||||
@ -688,7 +688,7 @@ export class CoreSitesProvider {
 | 
			
		||||
        oauthId?: number,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const db = await this.appDB;
 | 
			
		||||
        const entry = {
 | 
			
		||||
        const entry: SiteDBEntry = {
 | 
			
		||||
            id,
 | 
			
		||||
            siteUrl,
 | 
			
		||||
            token,
 | 
			
		||||
@ -1004,7 +1004,7 @@ export class CoreSitesProvider {
 | 
			
		||||
        const config = entry.config ? <CoreSiteConfig> CoreTextUtils.parseJSON(entry.config) : undefined;
 | 
			
		||||
 | 
			
		||||
        const site = new CoreSite(entry.id, entry.siteUrl, entry.token, info, entry.privateToken, config, entry.loggedOut == 1);
 | 
			
		||||
        site.setOAuthId(entry.oauthId);
 | 
			
		||||
        site.setOAuthId(entry.oauthId || undefined);
 | 
			
		||||
 | 
			
		||||
        return this.migrateSiteSchemas(site).then(() => {
 | 
			
		||||
            // Set site after migrating schemas, or a call to getSite could get the site while tables are being created.
 | 
			
		||||
@ -1221,11 +1221,16 @@ export class CoreSitesProvider {
 | 
			
		||||
    async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise<void> {
 | 
			
		||||
        const db = await this.appDB;
 | 
			
		||||
        const site = await this.getSite(siteId);
 | 
			
		||||
        const newValues = {
 | 
			
		||||
            token: '', // Erase the token for security.
 | 
			
		||||
        const newValues: Partial<SiteDBEntry> = {
 | 
			
		||||
            loggedOut: loggedOut ? 1 : 0,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (loggedOut) {
 | 
			
		||||
            // Erase the token for security.
 | 
			
		||||
            newValues.token = '';
 | 
			
		||||
            site.token = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        site.setLoggedOut(loggedOut);
 | 
			
		||||
 | 
			
		||||
        await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId });
 | 
			
		||||
@ -1266,7 +1271,7 @@ export class CoreSitesProvider {
 | 
			
		||||
    async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise<void> {
 | 
			
		||||
        const db = await this.appDB;
 | 
			
		||||
        const site = await this.getSite(siteId);
 | 
			
		||||
        const newValues = {
 | 
			
		||||
        const newValues: Partial<SiteDBEntry> = {
 | 
			
		||||
            token,
 | 
			
		||||
            privateToken,
 | 
			
		||||
            loggedOut: 0,
 | 
			
		||||
@ -1307,7 +1312,7 @@ export class CoreSitesProvider {
 | 
			
		||||
                // Error getting config, keep the current one.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const newValues: Record<string, string | number> = {
 | 
			
		||||
            const newValues: Partial<SiteDBEntry> = {
 | 
			
		||||
                info: JSON.stringify(info),
 | 
			
		||||
                loggedOut: site.isLoggedOut() ? 1 : 0,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ export class CoreUpdateManagerProvider {
 | 
			
		||||
 | 
			
		||||
        const versionApplied = await CoreConfig.get<number>(VERSION_APPLIED, 0);
 | 
			
		||||
 | 
			
		||||
        if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
 | 
			
		||||
        if (versionCode >= 3950 && versionApplied < 3950 && versionApplied > 0) {
 | 
			
		||||
            promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,8 @@
 | 
			
		||||
    <!-- add to homescreen for ios -->
 | 
			
		||||
    <meta name="apple-mobile-web-app-capable" content="yes" />
 | 
			
		||||
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
 | 
			
		||||
 | 
			
		||||
    <script src="assets/lib/mathjax/MathJax.js?delayStartupUntil=configured"></script>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user