commit
						4cf3d3d80d
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -28,6 +28,7 @@ npm-debug.log* | |||||||
| /platforms | /platforms | ||||||
| /plugins | /plugins | ||||||
| /www | /www | ||||||
|  | /src/assets/lib | ||||||
| 
 | 
 | ||||||
| /moodle.*.config.json | /moodle.*.config.json | ||||||
| !/moodle.example.config.json | !/moodle.example.config.json | ||||||
|  | |||||||
| @ -220,6 +220,9 @@ | |||||||
|                 <param name="android-package" value="org.apache.cordova.media.AudioHandler" /> |                 <param name="android-package" value="org.apache.cordova.media.AudioHandler" /> | ||||||
|             </feature> |             </feature> | ||||||
|         </config-file> |         </config-file> | ||||||
|  |         <config-file parent="/*" target="AndroidManifest.xml"> | ||||||
|  |             <uses-feature android:name="android.hardware.bluetooth" android:required="false" /> | ||||||
|  |         </config-file> | ||||||
|     </platform> |     </platform> | ||||||
|     <platform name="ios"> |     <platform name="ios"> | ||||||
|         <resource-file src="GoogleService-Info.plist" /> |         <resource-file src="GoogleService-Info.plist" /> | ||||||
|  | |||||||
| @ -3,5 +3,9 @@ | |||||||
|   "integrations": { |   "integrations": { | ||||||
|     "cordova": {} |     "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", |     "app_id": "com.moodle.moodlemobile", | ||||||
|     "appname": "Moodle Mobile", |     "appname": "Moodle Mobile", | ||||||
|     "versioncode": 3930, |     "versioncode": 3950, | ||||||
|     "versionname": "3.9.3-dev", |     "versionname": "3.9.5-dev", | ||||||
|     "cache_update_frequency_usually": 420000, |     "cache_update_frequency_usually": 420000, | ||||||
|     "cache_update_frequency_often": 1200000, |     "cache_update_frequency_often": 1200000, | ||||||
|     "cache_update_frequency_sometimes": 3600000, |     "cache_update_frequency_sometimes": 3600000, | ||||||
|  | |||||||
							
								
								
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -10531,14 +10531,14 @@ | |||||||
|       "optional": true |       "optional": true | ||||||
|     }, |     }, | ||||||
|     "fs-extra": { |     "fs-extra": { | ||||||
|       "version": "9.0.1", |       "version": "9.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", |       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", | ||||||
|       "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", |       "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "at-least-node": "^1.0.0", |         "at-least-node": "^1.0.0", | ||||||
|         "graceful-fs": "^4.2.0", |         "graceful-fs": "^4.2.0", | ||||||
|         "jsonfile": "^6.0.1", |         "jsonfile": "^6.0.1", | ||||||
|         "universalify": "^1.0.0" |         "universalify": "^2.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "fs-minipass": { |     "fs-minipass": { | ||||||
| @ -14192,12 +14192,12 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "jsonfile": { |     "jsonfile": { | ||||||
|       "version": "6.0.1", |       "version": "6.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", |       "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", | ||||||
|       "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", |       "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "graceful-fs": "^4.1.6", |         "graceful-fs": "^4.1.6", | ||||||
|         "universalify": "^1.0.0" |         "universalify": "^2.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "jsonparse": { |     "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": { |     "md5-file": { | ||||||
|       "version": "5.0.0", |       "version": "5.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", |       "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", | ||||||
| @ -21796,9 +21801,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "universalify": { |     "universalify": { | ||||||
|       "version": "1.0.0", |       "version": "2.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", | ||||||
|       "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" |       "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" | ||||||
|     }, |     }, | ||||||
|     "unorm": { |     "unorm": { | ||||||
|       "version": "1.6.0", |       "version": "1.6.0", | ||||||
|  | |||||||
| @ -110,6 +110,7 @@ | |||||||
|     "core-js": "^3.9.1", |     "core-js": "^3.9.1", | ||||||
|     "es6-promise-plugin": "^4.2.2", |     "es6-promise-plugin": "^4.2.2", | ||||||
|     "jszip": "^3.5.0", |     "jszip": "^3.5.0", | ||||||
|  |     "mathjax": "2.7.7", | ||||||
|     "moment": "^2.29.0", |     "moment": "^2.29.0", | ||||||
|     "nl.kingsquare.cordova.background-audio": "^1.0.1", |     "nl.kingsquare.cordova.background-audio": "^1.0.1", | ||||||
|     "phonegap-plugin-multidex": "^1.0.0", |     "phonegap-plugin-multidex": "^1.0.0", | ||||||
| @ -149,6 +150,7 @@ | |||||||
|     "eslint-plugin-prefer-arrow": "^1.2.2", |     "eslint-plugin-prefer-arrow": "^1.2.2", | ||||||
|     "eslint-plugin-promise": "^4.2.1", |     "eslint-plugin-promise": "^4.2.1", | ||||||
|     "faker": "^5.1.0", |     "faker": "^5.1.0", | ||||||
|  |     "fs-extra": "^9.1.0", | ||||||
|     "gulp": "4.0.2", |     "gulp": "4.0.2", | ||||||
|     "gulp-clip-empty-files": "^0.1.2", |     "gulp-clip-empty-files": "^0.1.2", | ||||||
|     "gulp-concat": "^2.6.1", |     "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')); |                     const equations = Array.from(container.querySelectorAll('.filter_mathjaxloader_equation')); | ||||||
|                     equations.forEach((node) => { |                     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.
 |                     // 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. |      * 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 { AddonModResourceModule } from './resource/resource.module'; | ||||||
| import { AddonModUrlModule } from './url/url.module'; | import { AddonModUrlModule } from './url/url.module'; | ||||||
| import { AddonModLtiModule } from './lti/lti.module'; | import { AddonModLtiModule } from './lti/lti.module'; | ||||||
|  | import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [], |     declarations: [], | ||||||
| @ -42,6 +43,7 @@ import { AddonModLtiModule } from './lti/lti.module'; | |||||||
|         AddonModFolderModule, |         AddonModFolderModule, | ||||||
|         AddonModImscpModule, |         AddonModImscpModule, | ||||||
|         AddonModLtiModule, |         AddonModLtiModule, | ||||||
|  |         AddonModH5PActivityModule, | ||||||
|     ], |     ], | ||||||
|     providers: [], |     providers: [], | ||||||
|     exports: [], |     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 stopClicksElements = Array.from(div.querySelectorAll('button,input,select,textarea')); | ||||||
|         const frames = Array.from(div.querySelectorAll(CoreIframeUtilsProvider.FRAME_TAGS.join(',').replace(/iframe,?/, ''))); |         const frames = Array.from(div.querySelectorAll(CoreIframeUtilsProvider.FRAME_TAGS.join(',').replace(/iframe,?/, ''))); | ||||||
|         const svgImages = Array.from(div.querySelectorAll('image')); |         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.
 |         // 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.
 |         // 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) => { |         iframes.forEach((iframe) => { | ||||||
|             this.treatIframe(iframe, site, canTreatVimeo); |             promises.push(this.treatIframe(iframe, site, canTreatVimeo)); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         svgImages.forEach((image) => { |         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.
 |             // 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)) { |         if (currentSite?.containsUrl(src)) { | ||||||
|             // URL points to current site, try to use auto-login.
 |             // URL points to current site, try to use auto-login.
 | ||||||
|             const finalUrl = await currentSite.getAutoLoginUrl(src, false); |             const finalUrl = await currentSite.getAutoLoginUrl(src, false); | ||||||
|  |             await CoreIframeUtils.fixIframeCookies(finalUrl); | ||||||
| 
 | 
 | ||||||
|             iframe.src = finalUrl; |             iframe.src = finalUrl; | ||||||
|             CoreIframeUtils.treatFrame(iframe, false); |             CoreIframeUtils.treatFrame(iframe, false); | ||||||
| @ -686,6 +690,8 @@ export class CoreFormatTextDirective implements OnChanges { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         await CoreIframeUtils.fixIframeCookies(src); | ||||||
|  | 
 | ||||||
|         if (site && src && canTreatVimeo) { |         if (site && src && canTreatVimeo) { | ||||||
|             // Check if it's a Vimeo video. If it is, use the wsplayer script instead to make restricted videos work.
 |             // 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]+)/); |             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(); |                     matches[1] + '&token=' + site.getToken(); | ||||||
| 
 | 
 | ||||||
|                 // Width and height are mandatory, we need to calculate them.
 |                 // Width and height are mandatory, we need to calculate them.
 | ||||||
|                 let width; |                 let width: string | number; | ||||||
|                 let height; |                 let height: string | number; | ||||||
| 
 | 
 | ||||||
|                 if (iframe.width) { |                 if (iframe.width) { | ||||||
|                     width = iframe.width; |                     width = iframe.width; | ||||||
| @ -719,13 +725,16 @@ export class CoreFormatTextDirective implements OnChanges { | |||||||
|                 if (site && !site.isVersionGreaterEqualThan('3.7')) { |                 if (site && !site.isVersionGreaterEqualThan('3.7')) { | ||||||
|                     newUrl += '&width=' + width + '&height=' + height; |                     newUrl += '&width=' + width + '&height=' + height; | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|  |                 await CoreIframeUtils.fixIframeCookies(src); | ||||||
|  | 
 | ||||||
|                 iframe.src = newUrl; |                 iframe.src = newUrl; | ||||||
| 
 | 
 | ||||||
|                 if (!iframe.width) { |                 if (!iframe.width) { | ||||||
|                     iframe.width = width; |                     iframe.width = String(width); | ||||||
|                 } |                 } | ||||||
|                 if (!iframe.height) { |                 if (!iframe.height) { | ||||||
|                     iframe.height = height; |                     iframe.height = String(height); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Do the iframe responsive.
 |                 // 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_FOLDER_SERVICES } from '@addons/mod/folder/folder.module'; | ||||||
| import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.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_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_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module'; | ||||||
| import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module'; | import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module'; | ||||||
| import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.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_FOLDER_SERVICES, | ||||||
|             ...ADDON_MOD_FORUM_SERVICES, |             ...ADDON_MOD_FORUM_SERVICES, | ||||||
|             // @todo ...ADDON_MOD_GLOSSARY_SERVICES,
 |             // @todo ...ADDON_MOD_GLOSSARY_SERVICES,
 | ||||||
|             // @todo ...ADDON_MOD_H5P_ACTIVITY_SERVICES,
 |             ...ADDON_MOD_H5P_ACTIVITY_SERVICES, | ||||||
|             ...ADDON_MOD_IMSCP_SERVICES, |             ...ADDON_MOD_IMSCP_SERVICES, | ||||||
|             ...ADDON_MOD_LESSON_SERVICES, |             ...ADDON_MOD_LESSON_SERVICES, | ||||||
|             ...ADDON_MOD_LTI_SERVICES, |             ...ADDON_MOD_LTI_SERVICES, | ||||||
|  | |||||||
| @ -226,7 +226,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | |||||||
|     async gotoBlog(): Promise<void> { |     async gotoBlog(): Promise<void> { | ||||||
|         const params: Params = { cmId: this.module.id }; |         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(); |         const loading = await CoreDomUtils.showModalLoading(); | ||||||
| 
 | 
 | ||||||
|         // Wait for site plugins to be fetched.
 |         // Wait for site plugins to be fetched.
 | ||||||
|         await CoreSitePlugins.waitFetchPlugins(); |         await CoreUtils.ignoreErrors(CoreSitePlugins.waitFetchPlugins()); | ||||||
| 
 | 
 | ||||||
|         if (!('format' in course) || typeof course.format == 'undefined') { |         if (!('format' in course) || typeof course.format == 'undefined') { | ||||||
|             const result = await CoreCourseHelper.getCourse(course.id); |             const result = await CoreCourseHelper.getCourse(course.id); | ||||||
| @ -1050,8 +1050,8 @@ export class CoreCourseProvider { | |||||||
| 
 | 
 | ||||||
|         if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) { |         if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) { | ||||||
|             // No custom format plugin. We don't need to wait for anything.
 |             // No custom format plugin. We don't need to wait for anything.
 | ||||||
|             await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params); |  | ||||||
|             loading.dismiss(); |             loading.dismiss(); | ||||||
|  |             await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params); | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @ -1084,6 +1084,8 @@ export class CoreCourseProvider { | |||||||
| 
 | 
 | ||||||
|             await CoreDomUtils.showConfirm(message, '', reload, ignore); |             await CoreDomUtils.showConfirm(message, '', reload, ignore); | ||||||
|             window.location.reload(); |             window.location.reload(); | ||||||
|  |         } finally { | ||||||
|  |             loading.dismiss(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -376,7 +376,7 @@ export class CoreH5PFileStorage { | |||||||
|      * @return Folder path. |      * @return Folder path. | ||||||
|      */ |      */ | ||||||
|     getCoreH5PPath(): string { |     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(); |         this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.isOfflineDisabledInSite(); | ||||||
| 
 | 
 | ||||||
|         // Send resize events when the page holding this component is re-entered.
 |         // Send resize events when the page holding this component is re-entered.
 | ||||||
|         // @todo: Check that this works as expected.
 |  | ||||||
|         this.currentPageRoute = router.url; |         this.currentPageRoute = router.url; | ||||||
|         this.subscription = router.events |         this.subscription = router.events | ||||||
|             .pipe(filter(event => event instanceof NavigationEnd)) |             .pipe(filter(event => event instanceof NavigationEnd)) | ||||||
|  | |||||||
| @ -431,7 +431,21 @@ export class CoreSitePluginsHelperProvider { | |||||||
|         styleEl.setAttribute('id', 'siteplugin-' + uniqueName); |         styleEl.setAttribute('id', 'siteplugin-' + uniqueName); | ||||||
|         styleEl.innerHTML = cssCode; |         styleEl.innerHTML = cssCode; | ||||||
| 
 | 
 | ||||||
|  |         // 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); |             document.head.appendChild(styleEl); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         // Styles have been loaded, now treat the CSS.
 |         // Styles have been loaded, now treat the CSS.
 | ||||||
|         CoreUtils.ignoreErrors( |         CoreUtils.ignoreErrors( | ||||||
|  | |||||||
| @ -211,11 +211,11 @@ export type SiteDBEntry = { | |||||||
|     id: string; |     id: string; | ||||||
|     siteUrl: string; |     siteUrl: string; | ||||||
|     token: string; |     token: string; | ||||||
|     info: string; |     info?: string | null; | ||||||
|     privateToken: string; |     privateToken: string; | ||||||
|     config: string; |     config?: string | null; | ||||||
|     loggedOut: number; |     loggedOut: number; | ||||||
|     oauthId: number; |     oauthId?: number | null; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type CurrentSiteDBEntry = { | export type CurrentSiteDBEntry = { | ||||||
|  | |||||||
| @ -1215,7 +1215,12 @@ export class CoreFileProvider { | |||||||
|      * @return Path. |      * @return Path. | ||||||
|      */ |      */ | ||||||
|     getWWWPath(): string { |     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) { |         if (position != -1) { | ||||||
|             return window.location.href.substr(0, position); |             return window.location.href.substr(0, position); | ||||||
|  | |||||||
| @ -688,7 +688,7 @@ export class CoreSitesProvider { | |||||||
|         oauthId?: number, |         oauthId?: number, | ||||||
|     ): Promise<void> { |     ): Promise<void> { | ||||||
|         const db = await this.appDB; |         const db = await this.appDB; | ||||||
|         const entry = { |         const entry: SiteDBEntry = { | ||||||
|             id, |             id, | ||||||
|             siteUrl, |             siteUrl, | ||||||
|             token, |             token, | ||||||
| @ -1004,7 +1004,7 @@ export class CoreSitesProvider { | |||||||
|         const config = entry.config ? <CoreSiteConfig> CoreTextUtils.parseJSON(entry.config) : undefined; |         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); |         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(() => { |         return this.migrateSiteSchemas(site).then(() => { | ||||||
|             // Set site after migrating schemas, or a call to getSite could get the site while tables are being created.
 |             // 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> { |     async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise<void> { | ||||||
|         const db = await this.appDB; |         const db = await this.appDB; | ||||||
|         const site = await this.getSite(siteId); |         const site = await this.getSite(siteId); | ||||||
|         const newValues = { |         const newValues: Partial<SiteDBEntry> = { | ||||||
|             token: '', // Erase the token for security.
 |  | ||||||
|             loggedOut: loggedOut ? 1 : 0, |             loggedOut: loggedOut ? 1 : 0, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|  |         if (loggedOut) { | ||||||
|  |             // Erase the token for security.
 | ||||||
|  |             newValues.token = ''; | ||||||
|  |             site.token = ''; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         site.setLoggedOut(loggedOut); |         site.setLoggedOut(loggedOut); | ||||||
| 
 | 
 | ||||||
|         await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); |         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> { |     async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise<void> { | ||||||
|         const db = await this.appDB; |         const db = await this.appDB; | ||||||
|         const site = await this.getSite(siteId); |         const site = await this.getSite(siteId); | ||||||
|         const newValues = { |         const newValues: Partial<SiteDBEntry> = { | ||||||
|             token, |             token, | ||||||
|             privateToken, |             privateToken, | ||||||
|             loggedOut: 0, |             loggedOut: 0, | ||||||
| @ -1307,7 +1312,7 @@ export class CoreSitesProvider { | |||||||
|                 // Error getting config, keep the current one.
 |                 // Error getting config, keep the current one.
 | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const newValues: Record<string, string | number> = { |             const newValues: Partial<SiteDBEntry> = { | ||||||
|                 info: JSON.stringify(info), |                 info: JSON.stringify(info), | ||||||
|                 loggedOut: site.isLoggedOut() ? 1 : 0, |                 loggedOut: site.isLoggedOut() ? 1 : 0, | ||||||
|             }; |             }; | ||||||
|  | |||||||
| @ -48,7 +48,7 @@ export class CoreUpdateManagerProvider { | |||||||
| 
 | 
 | ||||||
|         const versionApplied = await CoreConfig.get<number>(VERSION_APPLIED, 0); |         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()); |             promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -17,6 +17,8 @@ | |||||||
|     <!-- add to homescreen for ios --> |     <!-- add to homescreen for ios --> | ||||||
|     <meta name="apple-mobile-web-app-capable" content="yes" /> |     <meta name="apple-mobile-web-app-capable" content="yes" /> | ||||||
|     <meta name="apple-mobile-web-app-status-bar-style" content="black" /> |     <meta name="apple-mobile-web-app-status-bar-style" content="black" /> | ||||||
|  | 
 | ||||||
|  |     <script src="assets/lib/mathjax/MathJax.js?delayStartupUntil=configured"></script> | ||||||
| </head> | </head> | ||||||
| 
 | 
 | ||||||
| <body> | <body> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user