commit
						15fafef5f0
					
				
							
								
								
									
										33
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							| @ -26,7 +26,7 @@ jobs: | ||||
|     env: | ||||
|       MOODLE_DOCKER_DB: pgsql | ||||
|       MOODLE_DOCKER_BROWSER: chrome | ||||
|       MOODLE_DOCKER_PHP_VERSION: 7.4 | ||||
|       MOODLE_DOCKER_PHP_VERSION: '8.0' | ||||
|       MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'master' }} | ||||
|       MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }} | ||||
|       BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }} | ||||
| @ -42,6 +42,12 @@ jobs: | ||||
|         git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker | ||||
|     - name: Install npm packages | ||||
|       run: npm ci --no-audit | ||||
|     - name: Create Behat faildumps folder | ||||
|       run: | | ||||
|         mkdir moodle/behatfaildumps | ||||
|         chmod 777 moodle/behatfaildumps | ||||
|     - name: Install Behat Snapshots plugin | ||||
|       run: git clone --branch main --depth 1 https://github.com/NoelDeMartin/moodle-local_behatsnapshots $GITHUB_WORKSPACE/moodle/local/behatsnapshots | ||||
|     - name: Generate Behat tests plugin | ||||
|       run: | | ||||
|         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||
| @ -50,12 +56,21 @@ jobs: | ||||
|       run: | | ||||
|         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||
|         cp $GITHUB_WORKSPACE/moodle-docker/config.docker-template.php $GITHUB_WORKSPACE/moodle/config.php | ||||
|         sed -i "61c\$CFG->behat_faildump_path = '/var/www/html/behatfaildumps';" $GITHUB_WORKSPACE/moodle/config.php | ||||
|         sed -i "61i\$CFG->behat_increasetimeout = 2;" $GITHUB_WORKSPACE/moodle/config.php | ||||
|         sed -i "61i\$CFG->behat_ionic_wwwroot = 'http://moodleapp';" $GITHUB_WORKSPACE/moodle/config.php | ||||
|         sed -i "61i\$CFG->behat_snapshots_path = '/var/www/html/local/moodleappbehat/tests/behat/snapshots';" $GITHUB_WORKSPACE/moodle/config.php | ||||
|         echo "define('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER', 'http://bbbmockserver/hash' . sha1(\$CFG->behat_wwwroot));" >> $GITHUB_WORKSPACE/moodle/config.php | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose pull | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose up -d | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-wait-for-db | ||||
|     - name: Install Imagick PHP extension | ||||
|       run: | | ||||
|         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||
|         ./moodle-docker/bin/moodle-docker-compose exec webserver apt-get update | ||||
|         ./moodle-docker/bin/moodle-docker-compose exec webserver apt-get install -y libmagickwand-dev --no-install-recommends | ||||
|         ./moodle-docker/bin/moodle-docker-compose exec webserver pecl install imagick | ||||
|         ./moodle-docker/bin/moodle-docker-compose exec webserver docker-php-ext-enable imagick | ||||
|     - name: Compile & launch app with Docker | ||||
|       run: | | ||||
|         docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:behat . | ||||
| @ -65,8 +80,20 @@ jobs: | ||||
|     - name: Init Behat | ||||
|       run: | | ||||
|         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||
|          $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/init.php --parallel=8 --optimize-runs='@app&&$BEHAT_TAGS'" | ||||
|          $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/init.php --parallel=8 --optimize-runs='@app&&~@local&&$BEHAT_TAGS'" | ||||
|     - name: Run Behat tests | ||||
|       run: | | ||||
|         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&$BEHAT_TAGS' --auto-rerun=3" | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&~@local&&$BEHAT_TAGS' --auto-rerun=3" | ||||
|     - name: Upload Snapshot failures | ||||
|       uses: actions/upload-artifact@v3 | ||||
|       if: ${{ failure() }} | ||||
|       with: | ||||
|         name: snapshot_failures | ||||
|         path: moodle/local/moodleappbehat/tests/behat/snapshots/failures/* | ||||
|     - name: Upload Behat failures | ||||
|       uses: actions/upload-artifact@v3 | ||||
|       if: ${{ failure() }} | ||||
|       with: | ||||
|         name: behat_failures | ||||
|         path: moodle/behatfaildumps | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							| @ -8,7 +8,7 @@ jobs: | ||||
|     env: | ||||
|       MOODLE_DOCKER_DB: pgsql | ||||
|       MOODLE_DOCKER_BROWSER: chrome | ||||
|       MOODLE_DOCKER_PHP_VERSION: 7.4 | ||||
|       MOODLE_DOCKER_PHP_VERSION: '8.0' | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - uses: actions/setup-node@v3 | ||||
|  | ||||
| @ -6,6 +6,8 @@ WORKDIR /app | ||||
| # Prepare node dependencies | ||||
| RUN apt-get update && apt-get install libsecret-1-0 -y | ||||
| COPY package*.json ./ | ||||
| COPY patches ./patches | ||||
| RUN echo "unsafe-perm=true" > ./.npmrc | ||||
| RUN npm ci --no-audit | ||||
| 
 | ||||
| # Build source | ||||
|  | ||||
| @ -4,10 +4,12 @@ Moodle App | ||||
| This is the primary repository of source code for the official mobile app for Moodle. | ||||
| 
 | ||||
| * [User documentation](https://docs.moodle.org/en/Moodle_app) | ||||
| * [Developer documentation](http://docs.moodle.org/dev/Moodle_App) | ||||
| * [Development environment setup](https://docs.moodle.org/dev/Setting_up_your_development_environment_for_the_Moodle_App) | ||||
| * [Developer documentation](https://moodledev.io/general/app) | ||||
| * [Development environment setup](https://moodledev.io/general/app/development/setup) | ||||
| * [Bug Tracker](https://tracker.moodle.org/browse/MOBILE) | ||||
| * [Release Notes](https://docs.moodle.org/dev/Moodle_App_Release_Notes) | ||||
| * [Release Notes](https://moodledev.io/general/app_releases) | ||||
| 
 | ||||
| This project is tested with BrowserStack. | ||||
| 
 | ||||
| License | ||||
| ------- | ||||
|  | ||||
| @ -42,7 +42,8 @@ | ||||
|                 "input": "src/theme/theme.scss" | ||||
|               } | ||||
|             ], | ||||
|             "scripts": [] | ||||
|             "scripts": [], | ||||
|             "webWorkerTsConfig": "tsconfig.worker.json" | ||||
|           }, | ||||
|           "configurations": { | ||||
|             "production": { | ||||
| @ -50,6 +51,10 @@ | ||||
|                 { | ||||
|                   "replace": "src/testing/testing.module.ts", | ||||
|                   "with": "src/testing/testing.module.prod.ts" | ||||
|                 }, | ||||
|                 { | ||||
|                   "replace": "src/core/features/emulator/emulator.module.ts", | ||||
|                   "with": "src/core/features/emulator/emulator.module.prod.ts" | ||||
|                 } | ||||
|               ], | ||||
|               "optimization": { | ||||
|  | ||||
							
								
								
									
										12
									
								
								config.xml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								config.xml
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <widget android-versionCode="41001" id="com.moodle.moodlemobile" ios-CFBundleVersion="4.1.0.1" version="4.1.0" versionCode="41001" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0"> | ||||
| <widget android-versionCode="41100" id="com.moodle.moodlemobile" ios-CFBundleVersion="4.1.1.0" version="4.1.1" versionCode="41100" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0"> | ||||
|     <name>Moodle</name> | ||||
|     <description>Moodle official app</description> | ||||
|     <author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author> | ||||
| @ -27,7 +27,7 @@ | ||||
|     <preference name="UIWebViewBounce" value="false" /> | ||||
|     <preference name="DisallowOverscroll" value="true" /> | ||||
|     <preference name="prerendered-icon" value="true" /> | ||||
|     <preference name="AppendUserAgent" value="MoodleMobile 4.1.0 (41001)" /> | ||||
|     <preference name="AppendUserAgent" value="MoodleMobile 4.1.1 (41100)" /> | ||||
|     <preference name="BackupWebStorage" value="none" /> | ||||
|     <preference name="ScrollEnabled" value="false" /> | ||||
|     <preference name="KeyboardDisplayRequiresUserAction" value="false" /> | ||||
| @ -196,13 +196,9 @@ | ||||
|                 <param name="android-package" value="com.adobe.phonegap.push.PushPlugin" /> | ||||
|             </feature> | ||||
|         </config-file> | ||||
|         <config-file parent="/*" target="res/xml/config.xml"> | ||||
|             <feature name="Media"> | ||||
|                 <param name="android-package" value="org.apache.cordova.media.AudioHandler" /> | ||||
|             </feature> | ||||
|         </config-file> | ||||
|         <config-file parent="/*" target="AndroidManifest.xml"> | ||||
|             <uses-feature android:name="android.hardware.bluetooth" android:required="false" /> | ||||
|             <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> | ||||
|         </config-file> | ||||
|         <config-file parent="/*" target="AndroidManifest.xml"> | ||||
|             <queries> | ||||
| @ -236,7 +232,7 @@ | ||||
|             <true /> | ||||
|         </edit-config> | ||||
|         <edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString"> | ||||
|             <string>4.1.0</string> | ||||
|             <string>4.1.1</string> | ||||
|         </edit-config> | ||||
|         <edit-config file="*-Info.plist" mode="overwrite" target="CFBundleLocalizations"> | ||||
|             <array> | ||||
|  | ||||
| @ -71,5 +71,5 @@ gulp.task('watch', () => { | ||||
| }); | ||||
| 
 | ||||
| gulp.task('watch-behat', () => { | ||||
|     gulp.watch(['./src/**/*.feature', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); | ||||
|     gulp.watch(['./src/**/*.feature', './src/**/*.png', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); | ||||
| }); | ||||
|  | ||||
| @ -19,6 +19,7 @@ | ||||
| require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); | ||||
| require_once(__DIR__ . '/behat_app_helper.php'); | ||||
| 
 | ||||
| use Behat\Behat\Hook\Scope\ScenarioScope; | ||||
| use Behat\Gherkin\Node\TableNode; | ||||
| use Behat\Mink\Exception\DriverException; | ||||
| use Behat\Mink\Exception\ExpectationException; | ||||
| @ -45,6 +46,27 @@ class behat_app extends behat_app_helper { | ||||
| 
 | ||||
|     protected $windowsize = '360x720'; | ||||
| 
 | ||||
|     /** | ||||
|      * @BeforeScenario | ||||
|      */ | ||||
|     public function before_scenario(ScenarioScope $scope) { | ||||
|         if (!$scope->getFeature()->hasTag('app')) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         global $CFG; | ||||
| 
 | ||||
|         $performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null; | ||||
| 
 | ||||
|         if ($performanceLogs !== 'ALL') { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Enable DB Logging only for app tests with performance logs activated.
 | ||||
|         $this->getSession()->visit($this->get_app_url() . '/assets/env.json'); | ||||
|         $this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens the Moodle App in the browser and optionally logs in. | ||||
|      * | ||||
| @ -215,13 +237,21 @@ class behat_app extends behat_app_helper { | ||||
|     /** | ||||
|      * Trigger swipe gesture. | ||||
|      * | ||||
|      * @When /^I swipe to the (left|right) in the app$/ | ||||
|      * @When /^I swipe to the (left|right) (in (".+") )?in the app$/ | ||||
|      * @param string $direction Swipe direction | ||||
|      * @param bool $hasLocator Whether a reference locator is used. | ||||
|      * @param string $locator Reference locator. | ||||
|      */ | ||||
|     public function i_swipe_in_the_app(string $direction) { | ||||
|         $method = 'swipe' . ucwords($direction); | ||||
|     public function i_swipe_in_the_app(string $direction, bool $hasLocator = false, string $locator = '') { | ||||
|         if ($hasLocator) { | ||||
|             $locator = $this->parse_element_locator($locator); | ||||
|         } | ||||
| 
 | ||||
|         $this->zone_js("getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); | ||||
|         $result = $this->zone_js("swipe('$direction'" . ($hasLocator ? ", $locator" : '') . ')'); | ||||
| 
 | ||||
|         if ($result !== 'OK') { | ||||
|             throw new DriverException('Error when swiping - ' . $result); | ||||
|         } | ||||
| 
 | ||||
|         $this->wait_for_pending_js(); | ||||
| 
 | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| { | ||||
|     "app_id": "com.moodle.moodlemobile", | ||||
|     "appname": "Moodle Mobile", | ||||
|     "versioncode": 41001, | ||||
|     "versionname": "4.1.0", | ||||
|     "versioncode": 41100, | ||||
|     "versionname": "4.1.1", | ||||
|     "cache_update_frequency_usually": 420000, | ||||
|     "cache_update_frequency_often": 1200000, | ||||
|     "cache_update_frequency_sometimes": 3600000, | ||||
|  | ||||
							
								
								
									
										357
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										357
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "moodlemobile", | ||||
|   "version": "4.1.0", | ||||
|   "version": "4.1.1", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
| @ -3618,7 +3618,6 @@ | ||||
|       "version": "7.9.6", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz", | ||||
|       "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "regenerator-runtime": "^0.13.4" | ||||
|       } | ||||
| @ -4168,21 +4167,6 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/media": { | ||||
|       "version": "5.36.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/media/-/media-5.36.0.tgz", | ||||
|       "integrity": "sha512-WIDCeUlX7bCbse/x2Rr7mAIQJnLo18ZWcmsVgSTTBVS7ObU2DBl4ieqRx6y9PAAV+3tNZqMV4JAWDfMiFokpJg==", | ||||
|       "requires": { | ||||
|         "@types/cordova": "^0.0.34" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/cordova": { | ||||
|           "version": "0.0.34", | ||||
|           "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", | ||||
|           "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/media-capture": { | ||||
|       "version": "5.36.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/media-capture/-/media-capture-5.36.0.tgz", | ||||
| @ -4319,11 +4303,11 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@ionic/angular": { | ||||
|       "version": "5.9.2", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.9.2.tgz", | ||||
|       "integrity": "sha512-5GzKg+l4au3xFECky2v/USlRsmTAXgvNO5Zalt7NUXc//VJIL2lQvswojE6FBWuM/xR5W0CWbJdFth19TaZWVQ==", | ||||
|       "version": "5.9.4", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.9.4.tgz", | ||||
|       "integrity": "sha512-U/85FePF48VaZXTudTwpVXDqhGmYfarl/7vki7a4umnIORnWtHqD2/pXsqqZ/O1EcbALwULYIeVXAfkFpPd2wQ==", | ||||
|       "requires": { | ||||
|         "@ionic/core": "5.9.2", | ||||
|         "@ionic/core": "5.9.4", | ||||
|         "tslib": "^1.9.3" | ||||
|       }, | ||||
|       "dependencies": { | ||||
| @ -4666,9 +4650,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@ionic/core": { | ||||
|       "version": "5.9.2", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.2.tgz", | ||||
|       "integrity": "sha512-1ZqSBS8R6tGQsc+LsLxIRv0q3Ww6jwgJXLvdn6FmVWfpPbBvT+CjCuU9hqJ5qwM+atErblUMYSexvvpws8lGAA==", | ||||
|       "version": "5.9.4", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.4.tgz", | ||||
|       "integrity": "sha512-Ngz9yVT6fIiGdSxxBer8uJxP4w6PasvohYpLxhtMgYiWnyIu0vZra2ui3HrYukCzUo5/SbNPiUr1l7cj1E+7qw==", | ||||
|       "requires": { | ||||
|         "@stencil/core": "^2.4.0", | ||||
|         "ionicons": "^5.5.3", | ||||
| @ -5852,9 +5836,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@stencil/core": { | ||||
|       "version": "2.11.0", | ||||
|       "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.11.0.tgz", | ||||
|       "integrity": "sha512-/IubCWhVXCguyMUp/3zGrg3c882+RJNg/zpiKfyfJL3kRCOwe+/MD8OoAXVGdd+xAohZKIi1Ik+EHFlsptsjLg==" | ||||
|       "version": "2.22.2", | ||||
|       "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.2.tgz", | ||||
|       "integrity": "sha512-r+vbxsGNcBaV1VDOYW25lv4QfXTlNoIb5GpUX7rZ+cr59yqYCZC5tlV+IzX6YgHKW62ulCc9M3RYtTfHtNbNNw==" | ||||
|     }, | ||||
|     "@storybook/addon-controls": { | ||||
|       "version": "6.1.21", | ||||
| @ -9255,6 +9239,71 @@ | ||||
|         "eslint-visitor-keys": "^2.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "@videojs/http-streaming": { | ||||
|       "version": "2.15.1", | ||||
|       "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.15.1.tgz", | ||||
|       "integrity": "sha512-/uuN3bVkEeJAdrhu5Hyb19JoUo3CMys7yf2C1vUjeL1wQaZ4Oe8JrZzRrnWZ0rjvPgKfNLPXQomsRtgrMoRMJQ==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.12.5", | ||||
|         "@videojs/vhs-utils": "3.0.5", | ||||
|         "aes-decrypter": "3.1.3", | ||||
|         "global": "^4.4.0", | ||||
|         "m3u8-parser": "4.8.0", | ||||
|         "mpd-parser": "^0.22.1", | ||||
|         "mux.js": "6.0.1", | ||||
|         "video.js": "^6 || ^7" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@videojs/vhs-utils": { | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", | ||||
|       "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.12.5", | ||||
|         "global": "^4.4.0", | ||||
|         "url-toolkit": "^2.2.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@videojs/xhr": { | ||||
|       "version": "2.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz", | ||||
|       "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.5.5", | ||||
|         "global": "~4.4.0", | ||||
|         "is-function": "^1.0.1" | ||||
|       } | ||||
|     }, | ||||
|     "@webassemblyjs/ast": { | ||||
|       "version": "1.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", | ||||
| @ -9430,6 +9479,11 @@ | ||||
|         "@xtuc/long": "4.2.2" | ||||
|       } | ||||
|     }, | ||||
|     "@xmldom/xmldom": { | ||||
|       "version": "0.8.6", | ||||
|       "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz", | ||||
|       "integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==" | ||||
|     }, | ||||
|     "@xtuc/ieee754": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", | ||||
| @ -9567,6 +9621,32 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "aes-decrypter": { | ||||
|       "version": "3.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", | ||||
|       "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.12.5", | ||||
|         "@videojs/vhs-utils": "^3.0.5", | ||||
|         "global": "^4.4.0", | ||||
|         "pkcs7": "^1.0.4" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "agent-base": { | ||||
|       "version": "4.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", | ||||
| @ -14245,11 +14325,6 @@ | ||||
|       "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.2.0.tgz", | ||||
|       "integrity": "sha512-yDUG+9ieKVRitq5mGlNxjaZh/MgEhFFIgTIPhqSbUaQ8UuZbawy5mhJAVClqY97q8/rcQtL6dCDa7x2sEtCLcA==" | ||||
|     }, | ||||
|     "cordova-plugin-media": { | ||||
|       "version": "5.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/cordova-plugin-media/-/cordova-plugin-media-5.0.4.tgz", | ||||
|       "integrity": "sha512-mAqincYqOT5gu5LWyfgJu3qmOq+lhLAKhnOZULpG622FvYiHjjfsoJ/fkI55WwI3FIcHeeyhToGvHXBCNJePZg==" | ||||
|     }, | ||||
|     "cordova-plugin-media-capture": { | ||||
|       "version": "3.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.3.tgz", | ||||
| @ -15674,8 +15749,7 @@ | ||||
|     "dom-walk": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", | ||||
|       "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" | ||||
|     }, | ||||
|     "domain-browser": { | ||||
|       "version": "1.2.0", | ||||
| @ -16760,6 +16834,11 @@ | ||||
|       "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", | ||||
|       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" | ||||
|     }, | ||||
|     "event-target-shim": { | ||||
|       "version": "6.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", | ||||
|       "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==" | ||||
|     }, | ||||
|     "eventemitter3": { | ||||
|       "version": "4.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", | ||||
| @ -18627,7 +18706,6 @@ | ||||
|       "version": "4.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", | ||||
|       "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "min-document": "^2.19.0", | ||||
|         "process": "^0.11.10" | ||||
| @ -20030,6 +20108,11 @@ | ||||
|       "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "individual": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz", | ||||
|       "integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==" | ||||
|     }, | ||||
|     "infer-owner": { | ||||
|       "version": "1.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", | ||||
| @ -20545,8 +20628,7 @@ | ||||
|     "is-function": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", | ||||
|       "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" | ||||
|     }, | ||||
|     "is-generator-fn": { | ||||
|       "version": "2.1.0", | ||||
| @ -22371,6 +22453,11 @@ | ||||
|         "source-map-support": "^0.5.5" | ||||
|       } | ||||
|     }, | ||||
|     "keycode": { | ||||
|       "version": "2.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", | ||||
|       "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==" | ||||
|     }, | ||||
|     "keytar": { | ||||
|       "version": "7.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.2.0.tgz", | ||||
| @ -22940,6 +23027,31 @@ | ||||
|         "yallist": "^4.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "m3u8-parser": { | ||||
|       "version": "4.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", | ||||
|       "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.12.5", | ||||
|         "@videojs/vhs-utils": "^3.0.5", | ||||
|         "global": "^4.4.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "macos-release": { | ||||
|       "version": "2.5.0", | ||||
|       "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", | ||||
| @ -23425,7 +23537,6 @@ | ||||
|       "version": "2.19.0", | ||||
|       "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", | ||||
|       "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "dom-walk": "^0.1.0" | ||||
|       } | ||||
| @ -23708,6 +23819,41 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "mp3-mediarecorder": { | ||||
|       "version": "4.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/mp3-mediarecorder/-/mp3-mediarecorder-4.0.5.tgz", | ||||
|       "integrity": "sha512-tu8XvKGMrdwNmEQTzBbaJRLBAuVNEzbzmCOnYzUyYuEb48Kwl97qA6f5nBEaZXveNmHgvvi0i85TjROPC49qFA==", | ||||
|       "requires": { | ||||
|         "event-target-shim": "6.0.2", | ||||
|         "vmsg": "0.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "mpd-parser": { | ||||
|       "version": "0.22.1", | ||||
|       "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", | ||||
|       "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.12.5", | ||||
|         "@videojs/vhs-utils": "^3.0.5", | ||||
|         "@xmldom/xmldom": "^0.8.3", | ||||
|         "global": "^4.4.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "ms": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", | ||||
| @ -23740,6 +23886,30 @@ | ||||
|       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", | ||||
|       "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" | ||||
|     }, | ||||
|     "mux.js": { | ||||
|       "version": "6.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", | ||||
|       "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.11.2", | ||||
|         "global": "^4.4.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "nan": { | ||||
|       "version": "2.14.1", | ||||
|       "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", | ||||
| @ -24875,6 +25045,29 @@ | ||||
|       "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "ogv": { | ||||
|       "version": "1.8.9", | ||||
|       "resolved": "https://registry.npmjs.org/ogv/-/ogv-1.8.9.tgz", | ||||
|       "integrity": "sha512-tQA2E3E2PzdWqxIaI5X8q8Vxvj1Ap3JSZmD1MfnA+cTY3o0t+06zY4RKXckQ9pxeqGy/UH4l4QensssmbPLwAQ==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.16.7" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "on-finished": { | ||||
|       "version": "2.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", | ||||
| @ -25905,6 +26098,14 @@ | ||||
|         "node-modules-regexp": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "pkcs7": { | ||||
|       "version": "1.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", | ||||
|       "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.5.5" | ||||
|       } | ||||
|     }, | ||||
|     "pkg-dir": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", | ||||
| @ -26858,8 +27059,7 @@ | ||||
|     "process": { | ||||
|       "version": "0.11.10", | ||||
|       "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", | ||||
|       "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", | ||||
|       "dev": true | ||||
|       "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" | ||||
|     }, | ||||
|     "process-nextick-args": { | ||||
|       "version": "2.0.1", | ||||
| @ -28270,8 +28470,7 @@ | ||||
|     "regenerator-runtime": { | ||||
|       "version": "0.13.5", | ||||
|       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", | ||||
|       "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" | ||||
|     }, | ||||
|     "regenerator-transform": { | ||||
|       "version": "0.14.5", | ||||
| @ -28944,6 +29143,14 @@ | ||||
|         "aproba": "^1.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "rust-result": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz", | ||||
|       "integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==", | ||||
|       "requires": { | ||||
|         "individual": "^2.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "rxjs": { | ||||
|       "version": "6.5.5", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", | ||||
| @ -28964,6 +29171,14 @@ | ||||
|       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", | ||||
|       "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" | ||||
|     }, | ||||
|     "safe-json-parse": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz", | ||||
|       "integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==", | ||||
|       "requires": { | ||||
|         "rust-result": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "safe-regex": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", | ||||
| @ -32936,6 +33151,11 @@ | ||||
|         "prepend-http": "^2.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "url-toolkit": { | ||||
|       "version": "2.2.5", | ||||
|       "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", | ||||
|       "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" | ||||
|     }, | ||||
|     "use": { | ||||
|       "version": "3.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", | ||||
| @ -33118,6 +33338,54 @@ | ||||
|         "extsprintf": "^1.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "video.js": { | ||||
|       "version": "7.21.1", | ||||
|       "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.1.tgz", | ||||
|       "integrity": "sha512-AvHfr14ePDHCfW5Lx35BvXk7oIonxF6VGhSxocmTyqotkQpxwYdmt4tnQSV7MYzNrYHb0GI8tJMt20NDkCQrxg==", | ||||
|       "requires": { | ||||
|         "@babel/runtime": "^7.12.5", | ||||
|         "@videojs/http-streaming": "2.15.1", | ||||
|         "@videojs/vhs-utils": "^3.0.4", | ||||
|         "@videojs/xhr": "2.6.0", | ||||
|         "aes-decrypter": "3.1.3", | ||||
|         "global": "^4.4.0", | ||||
|         "keycode": "^2.2.0", | ||||
|         "m3u8-parser": "4.8.0", | ||||
|         "mpd-parser": "0.22.1", | ||||
|         "mux.js": "6.0.1", | ||||
|         "safe-json-parse": "4.0.0", | ||||
|         "videojs-font": "3.2.0", | ||||
|         "videojs-vtt.js": "^0.15.4" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@babel/runtime": { | ||||
|           "version": "7.20.7", | ||||
|           "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", | ||||
|           "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", | ||||
|           "requires": { | ||||
|             "regenerator-runtime": "^0.13.11" | ||||
|           } | ||||
|         }, | ||||
|         "regenerator-runtime": { | ||||
|           "version": "0.13.11", | ||||
|           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||
|           "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "videojs-font": { | ||||
|       "version": "3.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", | ||||
|       "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==" | ||||
|     }, | ||||
|     "videojs-vtt.js": { | ||||
|       "version": "0.15.4", | ||||
|       "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz", | ||||
|       "integrity": "sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA==", | ||||
|       "requires": { | ||||
|         "global": "^4.3.1" | ||||
|       } | ||||
|     }, | ||||
|     "vinyl": { | ||||
|       "version": "2.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", | ||||
| @ -33213,6 +33481,11 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "vmsg": { | ||||
|       "version": "0.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/vmsg/-/vmsg-0.4.0.tgz", | ||||
|       "integrity": "sha512-46BBqRSfqdFGUpO2j+Hpz8T9YE5uWG0/PWal1PT+R1o8NEthtjG/XWl4HzbB8hIHpg/UtmKvsxL2OKQBrIYcHQ==" | ||||
|     }, | ||||
|     "w3c-hr-time": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", | ||||
|  | ||||
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "moodlemobile", | ||||
|   "version": "4.1.0", | ||||
|   "version": "4.1.1", | ||||
|   "description": "The official app for Moodle.", | ||||
|   "author": { | ||||
|     "name": "Moodle Pty Ltd.", | ||||
| @ -24,7 +24,7 @@ | ||||
|     "build": "ionic build", | ||||
|     "build:prod": "NODE_ENV=production ionic build --prod", | ||||
|     "build:test": "NODE_ENV=testing ionic build --configuration=testing", | ||||
|     "dev:android": "ionic cordova run android --livereload", | ||||
|     "dev:android": "ionic cordova run android --livereload --external --ssl", | ||||
|     "dev:ios": "ionic cordova run ios", | ||||
|     "prod:android": "NODE_ENV=production ionic cordova run android --prod", | ||||
|     "prod:ios": "NODE_ENV=production ionic cordova run ios --prod", | ||||
| @ -63,7 +63,6 @@ | ||||
|     "@ionic-native/ionic-webview": "5.36.0", | ||||
|     "@ionic-native/keyboard": "5.36.0", | ||||
|     "@ionic-native/local-notifications": "5.36.0", | ||||
|     "@ionic-native/media": "5.36.0", | ||||
|     "@ionic-native/media-capture": "5.36.0", | ||||
|     "@ionic-native/network": "5.36.0", | ||||
|     "@ionic-native/push": "5.36.0", | ||||
| @ -73,7 +72,7 @@ | ||||
|     "@ionic-native/status-bar": "5.36.0", | ||||
|     "@ionic-native/web-intent": "5.36.0", | ||||
|     "@ionic-native/zip": "5.36.0", | ||||
|     "@ionic/angular": "5.9.2", | ||||
|     "@ionic/angular": "5.9.4", | ||||
|     "@moodlehq/cordova-plugin-file-opener": "3.0.5-moodle.1", | ||||
|     "@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5", | ||||
|     "@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3", | ||||
| @ -104,7 +103,6 @@ | ||||
|     "cordova-plugin-file": "6.0.2", | ||||
|     "cordova-plugin-geolocation": "4.1.0", | ||||
|     "cordova-plugin-ionic-keyboard": "2.2.0", | ||||
|     "cordova-plugin-media": "5.0.4", | ||||
|     "cordova-plugin-media-capture": "3.0.3", | ||||
|     "cordova-plugin-network-information": "3.0.0", | ||||
|     "cordova-plugin-prevent-override": "1.0.1", | ||||
| @ -122,10 +120,13 @@ | ||||
|     "mathjax": "2.7.9", | ||||
|     "moment": "2.29.4", | ||||
|     "moment-timezone": "0.5.38", | ||||
|     "mp3-mediarecorder": "^4.0.5", | ||||
|     "nl.kingsquare.cordova.background-audio": "1.0.1", | ||||
|     "ogv": "1.8.9", | ||||
|     "rxjs": "6.5.5", | ||||
|     "ts-md5": "1.2.7", | ||||
|     "tslib": "2.3.1", | ||||
|     "video.js": "7.21.1", | ||||
|     "zone.js": "0.10.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
| @ -222,9 +223,6 @@ | ||||
|         "ANDROID_SUPPORT_V4_VERSION": "26.+" | ||||
|       }, | ||||
|       "cordova-plugin-media-capture": {}, | ||||
|       "cordova-plugin-media": { | ||||
|         "KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO" | ||||
|       }, | ||||
|       "cordova-plugin-network-information": {}, | ||||
|       "@moodlehq/cordova-plugin-qrscanner": {}, | ||||
|       "cordova-plugin-splashscreen": {}, | ||||
|  | ||||
							
								
								
									
										30
									
								
								patches/event-target-shim+6.0.2.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								patches/event-target-shim+6.0.2.patch
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| diff --git a/node_modules/event-target-shim/index.d.ts b/node_modules/event-target-shim/index.d.ts
 | ||||
| index 7a5bfc7..ba5e7d8 100644
 | ||||
| --- a/node_modules/event-target-shim/index.d.ts
 | ||||
| +++ b/node_modules/event-target-shim/index.d.ts
 | ||||
| @@ -359,7 +359,7 @@ export declare namespace defineCustomEventTarget {
 | ||||
|  	/** | ||||
|  	 * The interface of CustomEventTarget. | ||||
|  	 */ | ||||
| -	type CustomEventTarget<TEventMap extends Record<string, Event>, TMode extends "standard" | "strict"> = EventTarget<TEventMap, TMode> & defineEventAttribute.EventAttributes<any, TEventMap>;
 | ||||
| +	type CustomEventTarget<TEventMap extends Record<string, Event>, TMode extends "standard" | "strict"> = EventTarget<TEventMap, TMode> & defineEventAttribute.EventAttributes<any>;
 | ||||
|  } | ||||
|  /** | ||||
|   * Define an event attribute. | ||||
| @@ -368,14 +368,12 @@ export declare namespace defineCustomEventTarget {
 | ||||
|   * @param _eventClass Unused, but to infer `Event` class type. | ||||
|   * @deprecated Use `getEventAttributeValue`/`setEventAttributeValue` pair on your derived class instead because of static analysis friendly. | ||||
|   */ | ||||
| -export declare function defineEventAttribute<TEventTarget extends EventTarget, TEventType extends string, TEventConstrucor extends typeof Event>(target: TEventTarget, type: TEventType, _eventClass?: TEventConstrucor): asserts target is TEventTarget & defineEventAttribute.EventAttributes<TEventTarget, Record<TEventType, InstanceType<TEventConstrucor>>>;
 | ||||
| +export declare function defineEventAttribute<TEventTarget extends EventTarget, TEventType extends string, TEventConstrucor extends typeof Event>(target: TEventTarget, type: TEventType, _eventClass?: TEventConstrucor): asserts target is TEventTarget & defineEventAttribute.EventAttributes<TEventTarget>;
 | ||||
|  export declare namespace defineEventAttribute { | ||||
|  	/** | ||||
|  	 * Definition of event attributes. | ||||
|  	 */ | ||||
| -	type EventAttributes<TEventTarget extends EventTarget<any, any>, TEventMap extends Record<string, Event>> = {
 | ||||
| -		[P in string & keyof TEventMap as `on${P}`]: EventTarget.CallbackFunction<TEventTarget, TEventMap[P]> | null;
 | ||||
| -	};
 | ||||
| +	type EventAttributes<TEventTarget extends EventTarget<any, any>> = Record<string, EventTarget.CallbackFunction<TEventTarget, any> | null>;
 | ||||
|  } | ||||
|  /** | ||||
|   * Set the warning handler. | ||||
							
								
								
									
										91
									
								
								patches/mp3-mediarecorder+4.0.5.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								patches/mp3-mediarecorder+4.0.5.patch
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | ||||
| diff --git a/node_modules/mp3-mediarecorder/dist/index.es.js b/node_modules/mp3-mediarecorder/dist/index.es.js
 | ||||
| index 7a96961..82ec4e8 100644
 | ||||
| --- a/node_modules/mp3-mediarecorder/dist/index.es.js
 | ||||
| +++ b/node_modules/mp3-mediarecorder/dist/index.es.js
 | ||||
| @@ -357,8 +357,7 @@ class Event$1 {
 | ||||
|              InitEventWasCalledWhileDispatching.warn(); | ||||
|              return; | ||||
|          } | ||||
| -        internalDataMap.set(this, {
 | ||||
| -            ...data,
 | ||||
| +        internalDataMap.set(this, Object.assign({}, data, {
 | ||||
|              type: String(type), | ||||
|              bubbles: Boolean(bubbles), | ||||
|              cancelable: Boolean(cancelable), | ||||
| @@ -366,8 +365,8 @@ class Event$1 {
 | ||||
|              currentTarget: null, | ||||
|              stopPropagationFlag: false, | ||||
|              stopImmediatePropagationFlag: false, | ||||
| -            canceledFlag: false,
 | ||||
| -        });
 | ||||
| +            canceledFlag: false
 | ||||
| +        }));
 | ||||
|      } | ||||
|  } | ||||
|  //------------------------------------------------------------------------------ | ||||
| diff --git a/node_modules/mp3-mediarecorder/dist/index.es5.js b/node_modules/mp3-mediarecorder/dist/index.es5.js
 | ||||
| index 0caa82d..aa46cc2 100644
 | ||||
| --- a/node_modules/mp3-mediarecorder/dist/index.es5.js
 | ||||
| +++ b/node_modules/mp3-mediarecorder/dist/index.es5.js
 | ||||
| @@ -418,7 +418,7 @@ class Event$1 {
 | ||||
|        return; | ||||
|      } | ||||
|   | ||||
| -    internalDataMap.set(this, { ...data,
 | ||||
| +    internalDataMap.set(this, Object.assign({}, data, {
 | ||||
|        type: String(type), | ||||
|        bubbles: Boolean(bubbles), | ||||
|        cancelable: Boolean(cancelable), | ||||
| @@ -427,7 +427,7 @@ class Event$1 {
 | ||||
|        stopPropagationFlag: false, | ||||
|        stopImmediatePropagationFlag: false, | ||||
|        canceledFlag: false | ||||
| -    });
 | ||||
| +    }));
 | ||||
|    } | ||||
|   | ||||
|  } //------------------------------------------------------------------------------ | ||||
| diff --git a/node_modules/mp3-mediarecorder/dist/index.js b/node_modules/mp3-mediarecorder/dist/index.js
 | ||||
| index f7a517e..5f7f415 100644
 | ||||
| --- a/node_modules/mp3-mediarecorder/dist/index.js
 | ||||
| +++ b/node_modules/mp3-mediarecorder/dist/index.js
 | ||||
| @@ -418,7 +418,7 @@ class Event$1 {
 | ||||
|        return; | ||||
|      } | ||||
|   | ||||
| -    internalDataMap.set(this, { ...data,
 | ||||
| +    internalDataMap.set(this, Object.assign({}, data, {
 | ||||
|        type: String(type), | ||||
|        bubbles: Boolean(bubbles), | ||||
|        cancelable: Boolean(cancelable), | ||||
| @@ -427,7 +427,7 @@ class Event$1 {
 | ||||
|        stopPropagationFlag: false, | ||||
|        stopImmediatePropagationFlag: false, | ||||
|        canceledFlag: false | ||||
| -    });
 | ||||
| +    }));
 | ||||
|    } | ||||
|   | ||||
|  } //------------------------------------------------------------------------------ | ||||
| diff --git a/node_modules/mp3-mediarecorder/dist/index.umd.js b/node_modules/mp3-mediarecorder/dist/index.umd.js
 | ||||
| index 3f5f2a2..dd7783d 100644
 | ||||
| --- a/node_modules/mp3-mediarecorder/dist/index.umd.js
 | ||||
| +++ b/node_modules/mp3-mediarecorder/dist/index.umd.js
 | ||||
| @@ -418,7 +418,7 @@ class Event$1 {
 | ||||
|        return; | ||||
|      } | ||||
|   | ||||
| -    internalDataMap.set(this, { ...data,
 | ||||
| +    internalDataMap.set(this, Object.assign({}, data, {
 | ||||
|        type: String(type), | ||||
|        bubbles: Boolean(bubbles), | ||||
|        cancelable: Boolean(cancelable), | ||||
| @@ -427,7 +427,7 @@ class Event$1 {
 | ||||
|        stopPropagationFlag: false, | ||||
|        stopImmediatePropagationFlag: false, | ||||
|        canceledFlag: false | ||||
| -    });
 | ||||
| +    }));
 | ||||
|    } | ||||
|   | ||||
|  } //------------------------------------------------------------------------------ | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 57 KiB | 
| @ -76,37 +76,46 @@ async function main() { | ||||
|     }; | ||||
|     writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements)); | ||||
| 
 | ||||
|     // Copy feature files.
 | ||||
|     // Copy feature and snapshot files.
 | ||||
|     if (!excludeFeatures) { | ||||
|         const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; | ||||
|         copySync(projectPath('src'), behatTempFeaturesPath, { filter: isFeatureFileOrDirectory }); | ||||
|         copySync(projectPath('src'), behatTempFeaturesPath, { filter: shouldCopyFileOrDirectory }); | ||||
| 
 | ||||
|         const behatFeaturesPath = `${pluginPath}/tests/behat`; | ||||
|         if (!existsSync(behatFeaturesPath)) { | ||||
|             mkdirSync(behatFeaturesPath, {recursive: true}); | ||||
|         } | ||||
| 
 | ||||
|         for await (const featureFile of getDirectoryFiles(behatTempFeaturesPath)) { | ||||
|             const featurePath = dirname(featureFile); | ||||
|             if (!featurePath.endsWith('/tests/behat')) { | ||||
|         for await (const file of getDirectoryFiles(behatTempFeaturesPath)) { | ||||
|             const filePath = dirname(file); | ||||
| 
 | ||||
|             if (filePath.endsWith('/tests/behat/snapshots')) { | ||||
|                 renameSync(file, behatFeaturesPath + '/snapshots/' + basename(file)); | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const newPath = featurePath.substring(0, featurePath.length - ('/tests/behat'.length)); | ||||
|             if (!filePath.endsWith('/tests/behat')) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const newPath = filePath.substring(0, filePath.length - ('/tests/behat'.length)); | ||||
|             const searchRegExp = /\//g; | ||||
|             const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core'; | ||||
|             const featureFilename = prefix + '-' + basename(featureFile); | ||||
|             renameSync(featureFile, behatFeaturesPath + '/' + featureFilename); | ||||
|             const featureFilename = prefix + '-' + basename(file); | ||||
|             renameSync(file, behatFeaturesPath + '/' + featureFilename); | ||||
|         } | ||||
| 
 | ||||
|         rmSync(behatTempFeaturesPath, {recursive: true}); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function isFeatureFileOrDirectory(src) { | ||||
|     const stats = statSync(src); | ||||
| function shouldCopyFileOrDirectory(path) { | ||||
|     const stats = statSync(path); | ||||
| 
 | ||||
|     return stats.isDirectory() || extname(src) === '.feature'; | ||||
|     return stats.isDirectory() | ||||
|         || extname(path) === '.feature' | ||||
|         || extname(path) === '.png'; | ||||
| } | ||||
| 
 | ||||
| function isExcluded(file, exclusions) { | ||||
|  | ||||
| @ -27,7 +27,10 @@ const ASSETS = { | ||||
|     '/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', | ||||
|     '/node_modules/mp3-mediarecorder/dist/vmsg.wasm': '/lib/vmsg/vmsg.wasm', | ||||
|     '/src/core/features/h5p/assets': '/lib/h5p', | ||||
|     '/node_modules/ogv/dist': '/lib/ogv', | ||||
|     '/node_modules/video.js/dist/video-js.min.css': '/lib/video.js/video-js.min.css', | ||||
| }; | ||||
| 
 | ||||
| module.exports = function(ctx) { | ||||
|  | ||||
| @ -1652,6 +1652,13 @@ | ||||
|   "core.courses.totalcoursesearchresults": "local_moodlemobileapp", | ||||
|   "core.currentdevice": "local_moodlemobileapp", | ||||
|   "core.custom": "form", | ||||
|   "core.reportbuilder.modifiedby": "tool_reportbuilder", | ||||
|   "core.reportbuilder.reports": "moodle", | ||||
|   "core.reportbuilder.reportsource": "moodle", | ||||
|   "core.reportbuilder.timecreated": "moodle", | ||||
|   "core.reportbuilder.filtersapplied": "local_moodlemobileapp", | ||||
|   "core.reportbuilder.showcolumns": "local_moodlemobileapp", | ||||
|   "core.reportbuilder.hidecolumns": "local_moodlemobileapp", | ||||
|   "core.datastoredoffline": "local_moodlemobileapp", | ||||
|   "core.date": "moodle", | ||||
|   "core.datecreated": "repository", | ||||
| @ -1735,9 +1742,11 @@ | ||||
|   "core.filenotfound": "resource", | ||||
|   "core.fileuploader.addfiletext": "repository", | ||||
|   "core.fileuploader.audio": "local_moodlemobileapp", | ||||
|   "core.fileuploader.audiotitle": "tiny_recordrtc", | ||||
|   "core.fileuploader.camera": "local_moodlemobileapp", | ||||
|   "core.fileuploader.confirmuploadfile": "local_moodlemobileapp", | ||||
|   "core.fileuploader.confirmuploadunknownsize": "local_moodlemobileapp", | ||||
|   "core.fileuploader.discardrecording": "local_moodlemobileapp", | ||||
|   "core.fileuploader.errorcapturingaudio": "local_moodlemobileapp", | ||||
|   "core.fileuploader.errorcapturingimage": "local_moodlemobileapp", | ||||
|   "core.fileuploader.errorcapturingvideo": "local_moodlemobileapp", | ||||
| @ -1751,11 +1760,18 @@ | ||||
|   "core.fileuploader.fileuploaded": "local_moodlemobileapp", | ||||
|   "core.fileuploader.invalidfiletype": "repository", | ||||
|   "core.fileuploader.maxbytesfile": "local_moodlemobileapp", | ||||
|   "core.fileuploader.microphonepermissiondenied": "local_moodlemobileapp", | ||||
|   "core.fileuploader.microphonepermissionrestricted": "local_moodlemobileapp", | ||||
|   "core.fileuploader.more": "data", | ||||
|   "core.fileuploader.pauserecording": "local_moodlemobileapp", | ||||
|   "core.fileuploader.photoalbums": "local_moodlemobileapp", | ||||
|   "core.fileuploader.readingfile": "local_moodlemobileapp", | ||||
|   "core.fileuploader.readingfileperc": "local_moodlemobileapp", | ||||
|   "core.fileuploader.resumerecording": "local_moodlemobileapp", | ||||
|   "core.fileuploader.selectafile": "local_moodlemobileapp", | ||||
|   "core.fileuploader.startrecording": "tiny_recordrtc", | ||||
|   "core.fileuploader.startrecordinginstructions": "local_moodlemobileapp", | ||||
|   "core.fileuploader.stoprecording": "tiny_recordrtc", | ||||
|   "core.fileuploader.uploadafile": "local_moodlemobileapp", | ||||
|   "core.fileuploader.uploading": "local_moodlemobileapp", | ||||
|   "core.fileuploader.uploadingperc": "local_moodlemobileapp", | ||||
| @ -2098,6 +2114,7 @@ | ||||
|   "core.nopasswordchangeforced": "local_moodlemobileapp", | ||||
|   "core.nopermissionerror": "local_moodlemobileapp", | ||||
|   "core.nopermissions": "error", | ||||
|   "core.nopermissiontoaccesspage": "error", | ||||
|   "core.noresults": "moodle", | ||||
|   "core.noselection": "form", | ||||
|   "core.notapplicable": "local_moodlemobileapp", | ||||
|  | ||||
| @ -33,7 +33,7 @@ | ||||
|             </ion-item-divider> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.name' | translate}}</h2> | ||||
|                     <p class="item-heading">{{ 'core.name' | translate}}</p> | ||||
|                     <p>{{ user.fullname }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
| @ -48,13 +48,13 @@ | ||||
|                 </ion-item-divider> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.issuername"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.issuername' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.issuername' | translate}}</p> | ||||
|                         <p>{{ badge.issuername }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.issuercontact"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.contact' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.contact' | translate}}</p> | ||||
|                         <p><a href="mailto:{{badge.issuercontact}}" core-link auto-login="no" [showBrowserWarning]="false"> | ||||
|                                 {{ badge.issuercontact }} | ||||
|                             </a></p> | ||||
| @ -70,37 +70,37 @@ | ||||
|                 </ion-item-divider> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.name"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'core.name' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'core.name' | translate}}</p> | ||||
|                         <p>{{ badge.name }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.version"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.version' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.version' | translate}}</p> | ||||
|                         <p>{{ badge.version }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.language"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.language' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.language' | translate}}</p> | ||||
|                         <p>{{ badge.language }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.description"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'core.description' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'core.description' | translate}}</p> | ||||
|                         <p>{{ badge.description }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.imageauthorname"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.imageauthorname' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.imageauthorname' | translate}}</p> | ||||
|                         <p>{{ badge.imageauthorname }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.imageauthoremail"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.imageauthoremail' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.imageauthoremail' | translate}}</p> | ||||
|                         <p><a href="mailto:{{badge.imageauthoremail}}" core-link auto-login="no" [showBrowserWarning]="false"> | ||||
|                                 {{ badge.imageauthoremail }} | ||||
|                             </a></p> | ||||
| @ -108,19 +108,19 @@ | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.imageauthorurl"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.imageauthorurl' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.imageauthorurl' | translate}}</p> | ||||
|                         <p><a [href]="badge.imageauthorurl" core-link auto-login="no"> {{ badge.imageauthorurl }} </a></p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.imagecaption"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.imagecaption' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.imagecaption' | translate}}</p> | ||||
|                         <p>{{ badge.imagecaption }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="course"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'core.course' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'core.course' | translate}}</p> | ||||
|                         <p> | ||||
|                             <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="courseId"> | ||||
|                             </core-format-text> | ||||
| @ -138,13 +138,13 @@ | ||||
|                 </ion-item-divider> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.dateissued"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.dateawarded' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.dateawarded' | translate}}</p> | ||||
|                         <p>{{badge.dateissued * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.dateexpire"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.expirydate' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.expirydate' | translate}}</p> | ||||
|                         <p> | ||||
|                             {{ badge.dateexpire * 1000 | coreFormatDate }} | ||||
|                             <span class="text-danger" *ngIf="currentTime >= badge.dateexpire"> | ||||
| @ -165,13 +165,13 @@ | ||||
|                 </ion-item-divider> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issuername"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.issuername' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.issuername' | translate}}</p> | ||||
|                         <p>{{ badge.endorsement.issuername }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issueremail"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.issueremail' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.issueremail' | translate}}</p> | ||||
|                         <p> | ||||
|                             <a href="mailto:{{badge.endorsement.issueremail}}" core-link auto-login="no" [showBrowserWarning]="false"> | ||||
|                                 {{ badge.endorsement.issueremail }} | ||||
| @ -181,25 +181,25 @@ | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issuerurl"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.issuerurl' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.issuerurl' | translate}}</p> | ||||
|                         <p><a [href]="badge.endorsement.issuerurl" core-link auto-login="no"> {{ badge.endorsement.issuerurl }} </a></p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.endorsement.dateissued"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.dateawarded' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.dateawarded' | translate}}</p> | ||||
|                         <p>{{ badge.endorsement.dateissued * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.endorsement.claimid"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.claimid' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.claimid' | translate}}</p> | ||||
|                         <p><a [href]="badge.endorsement.claimid" core-link auto-login="no"> {{ badge.endorsement.claimid }} </a></p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.endorsement.claimcomment"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.claimcomment' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.claimcomment' | translate}}</p> | ||||
|                         <p>{{ badge.endorsement.claimcomment }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| @ -214,12 +214,12 @@ | ||||
|                 </ion-item-divider> | ||||
|                 <ion-item class="ion-text-wrap" *ngFor="let relatedBadge of badge.relatedbadges"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ relatedBadge.name }}</h2> | ||||
|                         <p class="item-heading">{{ relatedBadge.name }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.relatedbadges.length == 0"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.norelated' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.norelated' | translate}}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|             </ion-item-group> | ||||
| @ -234,12 +234,12 @@ | ||||
|                 <ion-item class="ion-text-wrap" *ngFor="let alignment of badge.alignment" [href]="alignment.targeturl" core-link | ||||
|                     auto-login="no"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ alignment.targetname }}</h2> | ||||
|                         <p class="item-heading">{{ alignment.targetname }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="badge.alignment.length == 0"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.badges.noalignment' | translate}}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.badges.noalignment' | translate}}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|             </ion-item-group> | ||||
|  | ||||
| @ -25,11 +25,11 @@ Feature: Timeline block. | ||||
|       | assign   | C1                   | assign03  | Assignment 03 | ##tomorrow##   | | ||||
|       | assign   | C2                   | assign04  | Assignment 04 | ##+2 days##    | | ||||
|       | assign   | C1                   | assign05  | Assignment 05 | ##+5 days##    | | ||||
|       | assign   | C2                   | assign06  | Assignment 06 | ##+1 month##   | | ||||
|       | assign   | C2                   | assign07  | Assignment 07 | ##+1 month##   | | ||||
|       | assign   | C3                   | assign08  | Assignment 08 | ##+1 month##   | | ||||
|       | assign   | C2                   | assign09  | Assignment 09 | ##+1 month##   | | ||||
|       | assign   | C1                   | assign10  | Assignment 10 | ##+1 month##   | | ||||
|       | assign   | C2                   | assign06  | Assignment 06 | ##+31 days##   | | ||||
|       | assign   | C2                   | assign07  | Assignment 07 | ##+31 days##   | | ||||
|       | assign   | C3                   | assign08  | Assignment 08 | ##+31 days##   | | ||||
|       | assign   | C2                   | assign09  | Assignment 09 | ##+31 days##   | | ||||
|       | assign   | C1                   | assign10  | Assignment 10 | ##+31 days##   | | ||||
|       | assign   | C1                   | assign11  | Assignment 11 | ##+6 months##  | | ||||
|       | assign   | C1                   | assign12  | Assignment 12 | ##+6 months##  | | ||||
|       | assign   | C1                   | assign13  | Assignment 13 | ##+6 months##  | | ||||
|  | ||||
| @ -25,11 +25,11 @@ Feature: Timeline block. | ||||
|       | assign   | C1                   | assign03  | Assignment 03 | ##tomorrow##   | | ||||
|       | assign   | C2                   | assign04  | Assignment 04 | ##+2 days##    | | ||||
|       | assign   | C1                   | assign05  | Assignment 05 | ##+5 days##    | | ||||
|       | assign   | C2                   | assign06  | Assignment 06 | ##+1 month##   | | ||||
|       | assign   | C2                   | assign07  | Assignment 07 | ##+1 month##   | | ||||
|       | assign   | C3                   | assign08  | Assignment 08 | ##+1 month##   | | ||||
|       | assign   | C2                   | assign09  | Assignment 09 | ##+1 month##   | | ||||
|       | assign   | C1                   | assign10  | Assignment 10 | ##+1 month##   | | ||||
|       | assign   | C2                   | assign06  | Assignment 06 | ##+31 days##   | | ||||
|       | assign   | C2                   | assign07  | Assignment 07 | ##+31 days##   | | ||||
|       | assign   | C3                   | assign08  | Assignment 08 | ##+31 days##   | | ||||
|       | assign   | C2                   | assign09  | Assignment 09 | ##+31 days##   | | ||||
|       | assign   | C1                   | assign10  | Assignment 10 | ##+31 days##   | | ||||
|       | assign   | C1                   | assign11  | Assignment 11 | ##+6 months##  | | ||||
|       | assign   | C1                   | assign12  | Assignment 12 | ##+6 months##  | | ||||
|       | assign   | C1                   | assign13  | Assignment 13 | ##+6 months##  | | ||||
|  | ||||
| @ -27,19 +27,22 @@ | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <core-user-avatar [user]="entry.user" slot="start" [courseId]="entry.courseid"></core-user-avatar> | ||||
|                     <ion-label> | ||||
|                         <p class="item-heading"> | ||||
|                             <core-format-text [text]="entry.subject" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"> | ||||
|                             </core-format-text> | ||||
|                             <ion-note class="ion-float-end ion-padding-start ion-text-end"> | ||||
|                         <div class="flex-row ion-justify-content-between ion-align-items-center"> | ||||
|                             <h2> | ||||
|                                 <core-format-text [text]="entry.subject" [contextLevel]="contextLevel" | ||||
|                                     [contextInstanceId]="contextInstanceId"> | ||||
|                                 </core-format-text> | ||||
|                             </h2> | ||||
|                             <ion-note class="ion-text-end"> | ||||
|                                 {{ 'addon.blog.' + entry.publishTranslated! | translate}} | ||||
|                             </ion-note> | ||||
|                         </p> | ||||
|                         <p> | ||||
|                             <ion-note class="ion-float-end ion-text-end"> | ||||
|                         </div> | ||||
|                         <div class="flex-row ion-justify-content-between ion-align-items-center"> | ||||
|                             {{entry.user && entry.user.fullname}} | ||||
|                             <ion-note class="ion-text-end"> | ||||
|                                 {{entry.created | coreDateDayOrTime}} | ||||
|                             </ion-note> | ||||
|                             {{entry.user && entry.user!.fullname}} | ||||
|                         </p> | ||||
|                         </div> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-card-content> | ||||
|  | ||||
| @ -33,7 +33,7 @@ | ||||
|         </ion-grid> | ||||
| 
 | ||||
|         <core-swipe-slides [manager]="manager"> | ||||
|             <ng-template let-month="item"> | ||||
|             <ng-template let-month="item" let-activeView="active"> | ||||
|                 <!-- Calendar view. --> | ||||
|                 <ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname"> | ||||
|                     <div role="rowgroup"> | ||||
| @ -57,9 +57,9 @@ | ||||
|                                     "today": month.isCurrentMonth && day.istoday, | ||||
|                                     "weekend": day.isweekend, | ||||
|                                     "duration_finish": day.haslastdayofevent | ||||
|                                 }' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" tabindex="0" | ||||
|                                 (ariaButtonClick)="dayClicked(day.mday)"> | ||||
|                                 <p class="addon-calendar-day-number" role="button"> | ||||
|                                 }' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" | ||||
|                                 (ariaButtonClick)="dayClicked(day.mday)" [tabindex]="activeView ? 0 : -1"> | ||||
|                                 <p class="addon-calendar-day-number"> | ||||
|                                     <span aria-hidden="true">{{ day.mday }}</span> | ||||
|                                     <span class="sr-only">{{ day.periodName | translate }}</span> | ||||
|                                 </p> | ||||
| @ -72,8 +72,8 @@ | ||||
|                                 <div class="ion-hide-md-down addon-calendar-day-events" *ngIf="day.filteredEvents"> | ||||
|                                     <ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index"> | ||||
|                                         <div *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event" | ||||
|                                             [class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0" | ||||
|                                             (ariaButtonClick)="eventClicked(event, $event)"> | ||||
|                                             [class.addon-calendar-event-past]="event.ispast" (ariaButtonClick)="eventClicked(event, $event)" | ||||
|                                             [tabindex]="activeView ? 0 : -1"> | ||||
|                                             <span class="calendar_event_type calendar_event_{{event.formattedType}}"></span> | ||||
|                                             <ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock" | ||||
|                                                 [attr.aria-label]="'core.notsent' | translate"></ion-icon> | ||||
|  | ||||
| @ -25,7 +25,6 @@ | ||||
|         @include border-end(1px, solid var(--addon-calendar-border-color)); | ||||
|         overflow: hidden; | ||||
|         min-height: 60px; | ||||
|         cursor: pointer; | ||||
| 
 | ||||
|         &:first-child { | ||||
|             @include padding-horizontal(10px, null); | ||||
| @ -99,7 +98,7 @@ | ||||
| 
 | ||||
|     .addon-calendar-period { | ||||
|         flex-grow: 3; | ||||
|         h3 { | ||||
|         h2 { | ||||
|             margin-top: 10px; | ||||
|             font-size: 1.2rem; | ||||
|         } | ||||
|  | ||||
| @ -38,7 +38,7 @@ | ||||
|                         </ion-button> | ||||
|                     </ion-col> | ||||
|                     <ion-col class="ion-text-center addon-calendar-period"> | ||||
|                         <h3>{{ periodName }}</h3> | ||||
|                         <h2>{{ periodName }}</h2> | ||||
|                     </ion-col> | ||||
|                     <ion-col class="ion-text-end"> | ||||
|                         <ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'addon.calendar.daynext' | translate"> | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
| 
 | ||||
|     .addon-calendar-period { | ||||
|         flex-grow: 3; | ||||
|         h3 { | ||||
|         h2 { | ||||
|             margin-top: 10px; | ||||
|             font-size: 1.2rem; | ||||
|         } | ||||
|  | ||||
| @ -60,7 +60,7 @@ | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.calendar.when' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.calendar.when' | translate }}</p> | ||||
|                     <core-format-text [text]="event.formattedtime" [contextLevel]="event.contextLevel" | ||||
|                         [contextInstanceId]="event.contextInstanceId"></core-format-text> | ||||
|                 </ion-label> | ||||
| @ -70,13 +70,13 @@ | ||||
|             </ion-item> | ||||
|             <ion-item> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.calendar.eventtype' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.calendar.eventtype' | translate }}</p> | ||||
|                     <p>{{ 'addon.calendar.type' + event.formattedType | translate }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="courseName" [href]="courseUrl" core-link capture="true"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.course' | translate}}</h2> | ||||
|                     <p class="item-heading">{{ 'core.course' | translate}}</p> | ||||
|                     <p> | ||||
|                         <core-format-text [text]="courseName" contextLevel="course" [contextInstanceId]="courseId"> | ||||
|                         </core-format-text> | ||||
| @ -85,13 +85,13 @@ | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap core-groupname" *ngIf="groupName"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.group' | translate}}</h2> | ||||
|                     <p class="item-heading">{{ 'core.group' | translate}}</p> | ||||
|                     <p>{{ groupName }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="categoryPath"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.category' | translate}}</h2> | ||||
|                     <p class="item-heading">{{ 'core.category' | translate}}</p> | ||||
|                     <p> | ||||
|                         <core-format-text [text]="categoryPath" contextLevel="coursecat" [contextInstanceId]="event.categoryid"> | ||||
|                         </core-format-text> | ||||
| @ -100,7 +100,7 @@ | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="event.description"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.description' | translate}}</h2> | ||||
|                     <p class="item-heading">{{ 'core.description' | translate}}</p> | ||||
|                     <p> | ||||
|                         <core-format-text [text]="event.description" [contextLevel]="event.contextLevel" | ||||
|                             [contextInstanceId]="event.contextInstanceId"></core-format-text> | ||||
| @ -109,7 +109,7 @@ | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="event.location"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.location' | translate}}</h2> | ||||
|                     <p class="item-heading">{{ 'core.location' | translate}}</p> | ||||
|                     <p> | ||||
|                         <a [href]="event.encodedLocation" core-link auto-login="no"> | ||||
|                             <core-format-text [text]="event.location" [contextLevel]="event.contextLevel" | ||||
|  | ||||
| @ -19,7 +19,7 @@ | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ user.fullname }}</h2> | ||||
|                     <p class="item-heading">{{ user.fullname }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
| @ -115,7 +115,7 @@ | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <div *ngIf="competency"> | ||||
|             <h3 class="ion-margin-horizontal">{{ 'addon.competency.evidence' | translate }}</h3> | ||||
|             <h2 class="ion-margin-horizontal">{{ 'addon.competency.evidence' | translate }}</h2> | ||||
|             <p class="ion-margin-horizontal" *ngIf="competency.evidence.length == 0"> | ||||
|                 {{ 'addon.competency.noevidence' | translate }} | ||||
|             </p> | ||||
|  | ||||
| @ -53,7 +53,7 @@ | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ user.fullname }}</h2> | ||||
|                     <p class="item-heading">{{ user.fullname }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
|  | ||||
| @ -17,7 +17,7 @@ | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|                     <h2>{{ user.fullname }}</h2> | ||||
|                     <p class="item-heading">{{ user.fullname }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-card> | ||||
|  | ||||
| @ -16,20 +16,20 @@ | ||||
|         <ion-item class="ion-text-wrap" *ngIf="user"> | ||||
|             <core-user-avatar [user]="user" [courseId]="courseId" slot="start" [linkProfile]="false"></core-user-avatar> | ||||
|             <ion-label> | ||||
|                 <h2>{{user!.fullname}}</h2> | ||||
|                 <p class="item-heading">{{user.fullname}}</p> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <ion-card *ngIf="completion && tracked"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.coursecompletion.status' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.coursecompletion.status' | translate }}</p> | ||||
|                     <p>{{ statusText! | translate }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.coursecompletion.required' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.coursecompletion.required' | translate }}</p> | ||||
|                     <p *ngIf="completion.aggregation === 1">{{ 'addon.coursecompletion.criteriarequiredall' | translate }}</p> | ||||
|                     <p *ngIf="completion.aggregation === 2">{{ 'addon.coursecompletion.criteriarequiredany' | translate }}</p> | ||||
|                 </ion-label> | ||||
|  | ||||
							
								
								
									
										741
									
								
								src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										741
									
								
								src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,741 @@ | ||||
| // (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 { CorePlatform } from '@services/platform'; | ||||
| import { OGVPlayer, OGVCompat, OGVLoader } from 'ogv'; | ||||
| import videojs, { PreloadOption, TechSourceObject, VideoJSOptions } from 'video.js'; | ||||
| 
 | ||||
| const Tech = videojs.getComponent('Tech'); | ||||
| 
 | ||||
| /** | ||||
|  * Object.defineProperty but "lazy", which means that the value is only set after | ||||
|  * it retrieved the first time, rather than being set right away. | ||||
|  * | ||||
|  * @param obj The object to set the property on. | ||||
|  * @param key The key for the property to set. | ||||
|  * @param getValue The function used to get the value when it is needed. | ||||
|  * @param setter Whether a setter should be allowed or not. | ||||
|  * @returns Object. | ||||
|  */ | ||||
| const defineLazyProperty = <T>(obj: T, key: string, getValue: () => unknown, setter = true): T => { | ||||
|     const set = (value: unknown): void => { | ||||
|         Object.defineProperty(obj, key, { value, enumerable: true, writable: true }); | ||||
|     }; | ||||
| 
 | ||||
|     const options: PropertyDescriptor = { | ||||
|         configurable: true, | ||||
|         enumerable: true, | ||||
|         get() { | ||||
|             const value = getValue(); | ||||
| 
 | ||||
|             set(value); | ||||
| 
 | ||||
|             return value; | ||||
|         }, | ||||
|     }; | ||||
| 
 | ||||
|     if (setter) { | ||||
|         options.set = set; | ||||
|     } | ||||
| 
 | ||||
|     return Object.defineProperty(obj, key, options); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * OgvJS Media Controller for VideoJS - Wrapper for ogv.js Media API. | ||||
|  * | ||||
|  * Code adapted from https://github.com/HuongNV13/videojs-ogvjs/blob/f9b12bd53018d967bb305f02725834a98f20f61f/src/plugin.js
 | ||||
|  * Modified in the following ways: | ||||
|  * - Adapted to Typescript. | ||||
|  * - Use our own functions to detect the platform instead of using getDeviceOS. | ||||
|  * - Add an initialize static function. | ||||
|  * - In the play function, reset the media if it already ended to fix problems with replaying media. | ||||
|  * - Allow full screen in iOS devices, and implement enterFullScreen and exitFullScreen to use a fake full screen. | ||||
|  */ | ||||
| export class VideoJSOgvJS extends Tech { | ||||
| 
 | ||||
|     /** | ||||
|      * List of available events of the media player. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|     static readonly Events = [ | ||||
|         'loadstart', | ||||
|         'suspend', | ||||
|         'abort', | ||||
|         'error', | ||||
|         'emptied', | ||||
|         'stalled', | ||||
|         'loadedmetadata', | ||||
|         'loadeddata', | ||||
|         'canplay', | ||||
|         'canplaythrough', | ||||
|         'playing', | ||||
|         'waiting', | ||||
|         'seeking', | ||||
|         'seeked', | ||||
|         'ended', | ||||
|         'durationchange', | ||||
|         'timeupdate', | ||||
|         'progress', | ||||
|         'play', | ||||
|         'pause', | ||||
|         'ratechange', | ||||
|         'resize', | ||||
|         'volumechange', | ||||
|     ]; | ||||
| 
 | ||||
|     protected playerId?: string; | ||||
|     protected parentElement: HTMLElement | null = null; | ||||
|     protected placeholderElement = document.createElement('div'); | ||||
| 
 | ||||
|     // Variables/functions defined in parent classes.
 | ||||
|     protected el_!: OGVPlayerEl; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|     protected options_!: VideoJSOptions; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|     protected currentSource_?: TechSourceObject; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|     protected triggerReady!: () => void; | ||||
|     protected on!: (name: string, callback: (e?: Event) => void) => void; | ||||
| 
 | ||||
|     /** | ||||
|      * Create an instance of this Tech. | ||||
|      * | ||||
|      * @param options The key/value store of player options. | ||||
|      * @param ready Callback function to call when the `OgvJS` Tech is ready. | ||||
|      */ | ||||
|     constructor(options: VideoJSTechOptions, ready: () => void) { | ||||
|         super(options, ready); | ||||
| 
 | ||||
|         this.el_.src = options.src || options.source?.src || options.sources?.[0]?.src || this.el_.src; | ||||
|         VideoJSOgvJS.setIfAvailable(this.el_, 'autoplay', options.autoplay); | ||||
|         VideoJSOgvJS.setIfAvailable(this.el_, 'loop', options.loop); | ||||
|         VideoJSOgvJS.setIfAvailable(this.el_, 'poster', options.poster); | ||||
|         VideoJSOgvJS.setIfAvailable(this.el_, 'preload', options.preload); | ||||
|         this.playerId = options.playerId; | ||||
| 
 | ||||
|         this.on('loadedmetadata', () => { | ||||
|             if (CorePlatform.isIPhone()) { | ||||
|                 // iPhoneOS add some inline styles to the canvas, we need to remove it.
 | ||||
|                 const canvas = this.el_.getElementsByTagName('canvas')[0]; | ||||
| 
 | ||||
|                 canvas.style.removeProperty('width'); | ||||
|                 canvas.style.removeProperty('margin'); | ||||
|             } | ||||
| 
 | ||||
|             this.triggerReady(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the value for the player is it has that property. | ||||
|      * | ||||
|      * @param el HTML player. | ||||
|      * @param name Name of the property. | ||||
|      * @param value Value to set. | ||||
|      */ | ||||
|     static setIfAvailable(el: HTMLElement, name: string, value: unknown): void { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         if (el.hasOwnProperty(name)) { | ||||
|             el[name] = value; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if browser/device is supported by Ogv.JS. | ||||
|      * | ||||
|      * @returns Whether it's supported. | ||||
|      */ | ||||
|     static isSupported(): boolean { | ||||
|         return OGVCompat.supported('OGVPlayer'); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the tech can support the given type. | ||||
|      * | ||||
|      * @param type The mimetype to check. | ||||
|      * @returns 'probably', 'maybe', or '' (empty string). | ||||
|      */ | ||||
|     static canPlayType(type: string): string { | ||||
|         return (type.indexOf('/ogg') !== -1 || type.indexOf('/webm')) ? 'maybe' : ''; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the tech can support the given source. | ||||
|      * | ||||
|      * @param srcObj The source object. | ||||
|      * @returns The options passed to the tech. | ||||
|      */ | ||||
|     static canPlaySource(srcObj: TechSourceObject): string { | ||||
|         return VideoJSOgvJS.canPlayType(srcObj.type); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the volume can be changed in this browser/device. | ||||
|      * Volume cannot be changed in a lot of mobile devices. | ||||
|      * Specifically, it can't be changed from 1 on iOS. | ||||
|      * | ||||
|      * @returns True if volume can be controlled. | ||||
|      */ | ||||
|     static canControlVolume(): boolean { | ||||
|         if (CorePlatform.isIPhone()) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const player = new OGVPlayer(); | ||||
| 
 | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         return player.hasOwnProperty('volume'); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the volume can be muted in this browser/device. | ||||
|      * | ||||
|      * @returns True if volume can be muted. | ||||
|      */ | ||||
|     static canMuteVolume(): boolean { | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the playback rate can be changed in this browser/device. | ||||
|      * | ||||
|      * @returns True if playback rate can be controlled. | ||||
|      */ | ||||
|     static canControlPlaybackRate(): boolean { | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check to see if native 'TextTracks' are supported by this browser/device. | ||||
|      * | ||||
|      * @returns True if native 'TextTracks' are supported. | ||||
|      */ | ||||
|     static supportsNativeTextTracks(): boolean { | ||||
|         return false; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the fullscreen resize is supported by this browser/device. | ||||
|      * | ||||
|      * @returns True if the fullscreen resize is supported. | ||||
|      */ | ||||
|     static supportsFullscreenResize(): boolean { | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the progress events is supported by this browser/device. | ||||
|      * | ||||
|      * @returns True if the progress events is supported. | ||||
|      */ | ||||
|     static supportsProgressEvents(): boolean { | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the time update events is supported by this browser/device. | ||||
|      * | ||||
|      * @returns True if the time update events is supported. | ||||
|      */ | ||||
|     static supportsTimeupdateEvents(): boolean { | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Create the 'OgvJS' Tech's DOM element. | ||||
|      * | ||||
|      * @returns The element that gets created. | ||||
|      */ | ||||
|     createEl(): OGVPlayerEl { | ||||
|         const options = this.options_; | ||||
| 
 | ||||
|         if (options.base) { | ||||
|             OGVLoader.base = options.base; | ||||
|         } else if (!OGVLoader.base) { | ||||
|             throw new Error('Please specify the base for the ogv.js library'); | ||||
|         } | ||||
| 
 | ||||
|         const el = new OGVPlayer(options); | ||||
| 
 | ||||
|         el.className += ' vjs-tech'; | ||||
|         options.tag = el; | ||||
| 
 | ||||
|         return el; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start playback. | ||||
|      */ | ||||
|     play(): void { | ||||
|         if (this.ended()) { | ||||
|             // Reset the player, otherwise the Replay button doesn't work.
 | ||||
|             this.el_.stop(); | ||||
|         } | ||||
| 
 | ||||
|         this.el_.play(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the current playback speed. | ||||
|      * | ||||
|      * @returns Playback speed. | ||||
|      */ | ||||
|     playbackRate(): number { | ||||
|         return this.el_.playbackRate || 1; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the playback speed. | ||||
|      * | ||||
|      * @param val Speed for the player to play. | ||||
|      */ | ||||
|     setPlaybackRate(val: number): void { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         if (this.el_.hasOwnProperty('playbackRate')) { | ||||
|             this.el_.playbackRate = val; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a TimeRanges object that represents the ranges of the media resource that the user agent has played. | ||||
|      * | ||||
|      * @returns The range of points on the media timeline that has been reached through normal playback. | ||||
|      */ | ||||
|     played(): TimeRanges { | ||||
|         return this.el_.played; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pause playback. | ||||
|      */ | ||||
|     pause(): void { | ||||
|         this.el_.pause(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the player paused or not. | ||||
|      * | ||||
|      * @returns Whether is paused. | ||||
|      */ | ||||
|     paused(): boolean { | ||||
|         return this.el_.paused; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get current playing time. | ||||
|      * | ||||
|      * @returns Current time. | ||||
|      */ | ||||
|     currentTime(): number { | ||||
|         return this.el_.currentTime; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set current playing time. | ||||
|      * | ||||
|      * @param seconds Current time of audio/video. | ||||
|      */ | ||||
|     setCurrentTime(seconds: number): void { | ||||
|         try { | ||||
|             this.el_.currentTime = seconds; | ||||
|         } catch (e) { | ||||
|             videojs.log(e, 'Media is not ready. (Video.JS)'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get media's duration. | ||||
|      * | ||||
|      * @returns Duration. | ||||
|      */ | ||||
|     duration(): number { | ||||
|         if (this.el_.duration && this.el_.duration !== Infinity) { | ||||
|             return this.el_.duration; | ||||
|         } | ||||
| 
 | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a TimeRange object that represents the intersection | ||||
|      * of the time ranges for which the user agent has all | ||||
|      * relevant media. | ||||
|      * | ||||
|      * @returns Time ranges. | ||||
|      */ | ||||
|     buffered(): TimeRanges { | ||||
|         return this.el_.buffered; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get current volume level. | ||||
|      * | ||||
|      * @returns Volume. | ||||
|      */ | ||||
|     volume(): number { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         return this.el_.hasOwnProperty('volume') ? this.el_.volume : 1; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set current playing volume level. | ||||
|      * | ||||
|      * @param percentAsDecimal Volume percent as a decimal. | ||||
|      */ | ||||
|     setVolume(percentAsDecimal: number): void { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         if (!CorePlatform.isIPhone() && this.el_.hasOwnProperty('volume')) { | ||||
|             this.el_.volume = percentAsDecimal; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the player muted or not. | ||||
|      * | ||||
|      * @returns Whether it's muted. | ||||
|      */ | ||||
|     muted(): boolean { | ||||
|         return this.el_.muted; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Mute the player. | ||||
|      * | ||||
|      * @param muted True to mute the player. | ||||
|      */ | ||||
|     setMuted(muted: boolean): void { | ||||
|         this.el_.muted = !!muted; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the player muted by default or not. | ||||
|      * | ||||
|      * @returns Whether it's muted by default. | ||||
|      */ | ||||
|     defaultMuted(): boolean { | ||||
|         return this.el_.defaultMuted || false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the player width. | ||||
|      * | ||||
|      * @returns Width. | ||||
|      */ | ||||
|     width(): number { | ||||
|         return this.el_.offsetWidth; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the player height. | ||||
|      * | ||||
|      * @returns Height. | ||||
|      */ | ||||
|     height(): number { | ||||
|         return this.el_.offsetHeight; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the video width. | ||||
|      * | ||||
|      * @returns Video width. | ||||
|      */ | ||||
|     videoWidth(): number { | ||||
|         return (<HTMLVideoElement> this.el_).videoWidth ?? 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the video height. | ||||
|      * | ||||
|      * @returns Video heigth. | ||||
|      */ | ||||
|     videoHeight(): number { | ||||
|         return (<HTMLVideoElement> this.el_).videoHeight ?? 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get/set media source. | ||||
|      * | ||||
|      * @param src Source. | ||||
|      * @returns Source when getting it, undefined when setting it. | ||||
|      */ | ||||
|     src(src?: string): string | undefined { | ||||
|         if (typeof src === 'undefined') { | ||||
|             return this.el_.src; | ||||
|         } | ||||
| 
 | ||||
|         this.el_.src = src; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the media into the player. | ||||
|      */ | ||||
|     load(): void { | ||||
|         this.el_.load(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get current media source. | ||||
|      * | ||||
|      * @returns Current source. | ||||
|      */ | ||||
|     currentSrc(): string { | ||||
|         if (this.currentSource_) { | ||||
|             return this.currentSource_.src; | ||||
|         } | ||||
| 
 | ||||
|         return this.el_.currentSrc; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get media poster URL. | ||||
|      * | ||||
|      * @returns Poster. | ||||
|      */ | ||||
|     poster(): string { | ||||
|         return 'poster' in this.el_ ? this.el_.poster : ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set media poster URL. | ||||
|      * | ||||
|      * @param url The poster image's url. | ||||
|      */ | ||||
|     setPoster(url: string): void { | ||||
|         (<HTMLVideoElement> this.el_).poster = url; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the media preloaded or not. | ||||
|      * | ||||
|      * @returns Whether it's preloaded. | ||||
|      */ | ||||
|     preload(): PreloadOption { | ||||
|         return <PreloadOption> this.el_.preload || 'none'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the media preload method. | ||||
|      * | ||||
|      * @param val Value for preload attribute. | ||||
|      */ | ||||
|     setPreload(val: PreloadOption): void { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         if (this.el_.hasOwnProperty('preload')) { | ||||
|             this.el_.preload = val; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the media auto-played or not. | ||||
|      * | ||||
|      * @returns Whether it's auto-played. | ||||
|      */ | ||||
|     autoplay(): boolean { | ||||
|         return this.el_.autoplay || false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set media autoplay method. | ||||
|      * | ||||
|      * @param val Value for autoplay attribute. | ||||
|      */ | ||||
|     setAutoplay(val: boolean): void { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         if (this.el_.hasOwnProperty('autoplay')) { | ||||
|             this.el_.autoplay = !!val; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Does the media has controls or not. | ||||
|      * | ||||
|      * @returns Whether it has controls. | ||||
|      */ | ||||
|     controls(): boolean { | ||||
|         return this.el_.controls || false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the media controls method. | ||||
|      * | ||||
|      * @param val Value for controls attribute. | ||||
|      */ | ||||
|     setControls(val: boolean): void { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         if (this.el_.hasOwnProperty('controls')) { | ||||
|             this.el_.controls = !!val; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the media looped or not. | ||||
|      * | ||||
|      * @returns Whether it's looped. | ||||
|      */ | ||||
|     loop(): boolean { | ||||
|         return this.el_.loop || false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the media loop method. | ||||
|      * | ||||
|      * @param val Value for loop attribute. | ||||
|      */ | ||||
|     setLoop(val: boolean): void { | ||||
|         // eslint-disable-next-line no-prototype-builtins
 | ||||
|         if (this.el_.hasOwnProperty('loop')) { | ||||
|             this.el_.loop = !!val; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a TimeRanges object that represents the | ||||
|      * ranges of the media resource to which it is possible | ||||
|      * for the user agent to seek. | ||||
|      * | ||||
|      * @returns Time ranges. | ||||
|      */ | ||||
|     seekable(): TimeRanges { | ||||
|         return this.el_.seekable; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is player in the "seeking" state or not. | ||||
|      * | ||||
|      * @returns Whether is in the seeking state. | ||||
|      */ | ||||
|     seeking(): boolean { | ||||
|         return this.el_.seeking; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Is the media ended or not. | ||||
|      * | ||||
|      * @returns Whether it's ended. | ||||
|      */ | ||||
|     ended(): boolean { | ||||
|         return this.el_.ended; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the current state of network activity | ||||
|      * NETWORK_EMPTY (numeric value 0) | ||||
|      * NETWORK_IDLE (numeric value 1) | ||||
|      * NETWORK_LOADING (numeric value 2) | ||||
|      * NETWORK_NO_SOURCE (numeric value 3) | ||||
|      * | ||||
|      * @returns Network state. | ||||
|      */ | ||||
|     networkState(): number { | ||||
|         return this.el_.networkState; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the current state of the player. | ||||
|      * HAVE_NOTHING (numeric value 0) | ||||
|      * HAVE_METADATA (numeric value 1) | ||||
|      * HAVE_CURRENT_DATA (numeric value 2) | ||||
|      * HAVE_FUTURE_DATA (numeric value 3) | ||||
|      * HAVE_ENOUGH_DATA (numeric value 4) | ||||
|      * | ||||
|      * @returns Ready state. | ||||
|      */ | ||||
|     readyState(): number { | ||||
|         return this.el_.readyState; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Does the player support native fullscreen mode or not. (Mobile devices) | ||||
|      * | ||||
|      * @returns Whether it supports full screen. | ||||
|      */ | ||||
|     supportsFullScreen(): boolean { | ||||
|         return !!this.playerId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get media player error. | ||||
|      * | ||||
|      * @returns Error. | ||||
|      */ | ||||
|     error(): MediaError | null { | ||||
|         return this.el_.error; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Enter full screen mode. | ||||
|      */ | ||||
|     enterFullScreen(): void { | ||||
|         // Use a "fake" full screen mode, moving the player to a different place in DOM to be able to use full screen size.
 | ||||
|         const player = videojs.getPlayer(this.playerId ?? ''); | ||||
|         if (!player) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const container = player.el(); | ||||
|         this.parentElement = container.parentElement; | ||||
|         if (!this.parentElement) { | ||||
|             // Shouldn't happen, it means the element is not in DOM. Do not support full screen in this case.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.parentElement.replaceChild(this.placeholderElement, container); | ||||
|         document.body.appendChild(container); | ||||
|         container.classList.add('vjs-ios-moodleapp-fs'); | ||||
| 
 | ||||
|         player.isFullscreen(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Exit full screen mode. | ||||
|      */ | ||||
|     exitFullScreen(): void { | ||||
|         if (!this.parentElement) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const player = videojs.getPlayer(this.playerId ?? ''); | ||||
|         if (!player) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const container = player.el(); | ||||
|         this.parentElement.replaceChild(container, this.placeholderElement); | ||||
|         container.classList.remove('vjs-ios-moodleapp-fs'); | ||||
| 
 | ||||
|         player.isFullscreen(false); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| [ | ||||
|     ['featuresVolumeControl', 'canControlVolume'], | ||||
|     ['featuresMuteControl', 'canMuteVolume'], | ||||
|     ['featuresPlaybackRate', 'canControlPlaybackRate'], | ||||
|     ['featuresNativeTextTracks', 'supportsNativeTextTracks'], | ||||
|     ['featuresFullscreenResize', 'supportsFullscreenResize'], | ||||
|     ['featuresProgressEvents', 'supportsProgressEvents'], | ||||
|     ['featuresTimeupdateEvents', 'supportsTimeupdateEvents'], | ||||
| ].forEach(([key, fn]) => { | ||||
|     defineLazyProperty(VideoJSOgvJS.prototype, key, () => VideoJSOgvJS[fn](), true); | ||||
| }); | ||||
| 
 | ||||
| type OGVPlayerEl = (HTMLAudioElement | HTMLVideoElement) & { | ||||
|     stop: () => void; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * VideoJS Tech options. It includes some options added by VideoJS internally. | ||||
|  */ | ||||
| type VideoJSTechOptions = VideoJSOptions & { | ||||
|     playerId?: string; | ||||
| }; | ||||
| @ -26,7 +26,9 @@ import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin'; | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             useValue: () => CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance), | ||||
|             useValue: () => { | ||||
|                 CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
|  | ||||
| @ -12,12 +12,12 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonFilterMediaPluginVideoJS } from '@addons/filter/mediaplugin/services/videojs'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreMedia } from '@singletons/media'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support the Multimedia filter. | ||||
| @ -33,58 +33,38 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     filter( | ||||
|         text: string, | ||||
|     ): string | Promise<string> { | ||||
|     filter(text: string): string | Promise<string> { | ||||
|         this.template.innerHTML = text; | ||||
| 
 | ||||
|         const videos = Array.from(this.template.content.querySelectorAll('video')); | ||||
| 
 | ||||
|         videos.forEach((video) => { | ||||
|             this.treatVideoFilters(video); | ||||
|             AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video); | ||||
|         }); | ||||
| 
 | ||||
|         return this.template.innerHTML; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Treat video filters. Currently only treating youtube video using video JS. | ||||
|      * | ||||
|      * @param video Video element. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected treatVideoFilters(video: HTMLElement): void { | ||||
|         // Treat Video JS Youtube video links and translate them to iframes.
 | ||||
|         if (!video.classList.contains('video-js')) { | ||||
|             return; | ||||
|         } | ||||
|     handleHtml(container: HTMLElement): void { | ||||
|         const mediaElements = Array.from(container.querySelectorAll<HTMLVideoElement | HTMLAudioElement>('video, audio')); | ||||
| 
 | ||||
|         const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'; | ||||
|         const data = <VideoDataSetup> CoreTextUtils.parseJSON(dataSetupString, {}); | ||||
|         const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src); | ||||
|         mediaElements.forEach((mediaElement) => { | ||||
|             if (CoreMedia.mediaUsesJavascriptPlayer(mediaElement)) { | ||||
|                 AddonFilterMediaPluginVideoJS.createPlayer(mediaElement); | ||||
| 
 | ||||
|         if (!youtubeUrl) { | ||||
|             return; | ||||
|         } | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|         const iframe = document.createElement('iframe'); | ||||
|         iframe.id = video.id; | ||||
|         iframe.src = youtubeUrl; | ||||
|         iframe.setAttribute('frameborder', '0'); | ||||
|         iframe.setAttribute('allowfullscreen', '1'); | ||||
|         iframe.width = '100%'; | ||||
|         iframe.height = '300'; | ||||
| 
 | ||||
|         // Replace video tag by the iframe.
 | ||||
|         video.parentNode?.replaceChild(iframe, video); | ||||
|             // Remove the VideoJS classes and data if present.
 | ||||
|             mediaElement.classList.remove('video-js'); | ||||
|             mediaElement.removeAttribute('data-setup'); | ||||
|             mediaElement.removeAttribute('data-setup-lazy'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const AddonFilterMediaPluginHandler = makeSingleton(AddonFilterMediaPluginHandlerService); | ||||
| 
 | ||||
| type VideoDataSetup = { | ||||
|     techOrder?: string[]; | ||||
|     sources?: { | ||||
|         src?: string; | ||||
|     }[]; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										188
									
								
								src/addons/filter/mediaplugin/services/videojs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/addons/filter/mediaplugin/services/videojs.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,188 @@ | ||||
| // (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 { CorePromisedValue } from '@classes/promised-value'; | ||||
| import { CoreExternalContentDirective } from '@directives/external-content'; | ||||
| import { CoreLang } from '@services/lang'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreDirectivesRegistry } from '@singletons/directives-registry'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import type videojs from 'video.js'; | ||||
| 
 | ||||
| // eslint-disable-next-line no-duplicate-imports
 | ||||
| import type { VideoJSOptions, VideoJSPlayer } from 'video.js'; | ||||
| 
 | ||||
| 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 { | ||||
|         [VIDEO_JS_PLAYER_CREATED]: CoreEventJSVideoPlayerCreated; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const VIDEO_JS_PLAYER_CREATED = 'video_js_player_created'; | ||||
| 
 | ||||
| /** | ||||
|  * Wrapper encapsulating videojs functionality. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonFilterMediaPluginVideoJSService { | ||||
| 
 | ||||
|     protected videojs?: CorePromisedValue<typeof videojs>; | ||||
| 
 | ||||
|     /** | ||||
|      * Create a VideoJS player. | ||||
|      * | ||||
|      * @param element Media element. | ||||
|      */ | ||||
|     async createPlayer(element: HTMLVideoElement | HTMLAudioElement): Promise<void> { | ||||
|         // Wait for external-content to finish in the element and its sources.
 | ||||
|         await Promise.all([ | ||||
|             CoreDirectivesRegistry.waitDirectivesReady(element, undefined, CoreExternalContentDirective), | ||||
|             CoreDirectivesRegistry.waitDirectivesReady(element, 'source', CoreExternalContentDirective), | ||||
|         ]); | ||||
| 
 | ||||
|         // Create player.
 | ||||
|         const videojs = await this.getVideoJS(); | ||||
|         const dataSetupString = element.getAttribute('data-setup') || element.getAttribute('data-setup-lazy') || '{}'; | ||||
|         const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {}); | ||||
|         const player = videojs( | ||||
|             element, | ||||
|             { | ||||
|                 controls: true, | ||||
|                 techOrder: ['OgvJS'], | ||||
|                 language: await CoreLang.getCurrentLanguage(), | ||||
|                 controlBar: { pictureInPictureToggle: false }, | ||||
|                 aspectRatio: data.aspectRatio, | ||||
|             }, | ||||
|             () => element.tagName === 'VIDEO' && this.fixVideoJSPlayerSize(player), | ||||
|         ); | ||||
| 
 | ||||
|         CoreEvents.trigger(VIDEO_JS_PLAYER_CREATED, { | ||||
|             element, | ||||
|             player, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Find a VideoJS player by id. | ||||
|      * | ||||
|      * @param id Element id. | ||||
|      * @returns VideoJS player. | ||||
|      */ | ||||
|     async findPlayer(id: string): Promise<VideoJSPlayer | null> { | ||||
|         const videojs = await this.getVideoJS(); | ||||
| 
 | ||||
|         return videojs.getPlayer(id); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Treat Video JS Youtube video links and translate them to iframes. | ||||
|      * | ||||
|      * @param video Video element. | ||||
|      */ | ||||
|     treatYoutubeVideos(video: HTMLElement): void { | ||||
|         if (!video.classList.contains('video-js')) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'; | ||||
|         const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {}); | ||||
|         const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src); | ||||
| 
 | ||||
|         if (!youtubeUrl) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const iframe = document.createElement('iframe'); | ||||
|         iframe.id = video.id; | ||||
|         iframe.src = youtubeUrl; | ||||
|         iframe.setAttribute('frameborder', '0'); | ||||
|         iframe.setAttribute('allowfullscreen', '1'); | ||||
|         iframe.width = '100%'; | ||||
|         iframe.height = '300'; | ||||
| 
 | ||||
|         // Replace video tag by the iframe.
 | ||||
|         video.parentNode?.replaceChild(iframe, video); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets videojs instance. | ||||
|      * | ||||
|      * @returns VideoJS. | ||||
|      */ | ||||
|     protected async getVideoJS(): Promise<typeof videojs> { | ||||
|         if (!this.videojs) { | ||||
|             this.videojs = new CorePromisedValue(); | ||||
| 
 | ||||
|             // Inject CSS.
 | ||||
|             const link = document.createElement('link'); | ||||
| 
 | ||||
|             link.rel = 'stylesheet'; | ||||
|             link.href = 'assets/lib/video.js/video-js.min.css'; | ||||
| 
 | ||||
|             document.head.appendChild(link); | ||||
| 
 | ||||
|             // Load library.
 | ||||
|             return import('@addons/filter/mediaplugin/utils/videojs').then(({ initializeVideoJSOgvJS, videojs }) => { | ||||
|                 initializeVideoJSOgvJS(); | ||||
| 
 | ||||
|                 this.videojs?.resolve(videojs); | ||||
| 
 | ||||
|                 return videojs; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return this.videojs; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fix VideoJS player size. | ||||
|      * If video width is wider than available width, video is cut off. Fix the dimensions in this case. | ||||
|      * | ||||
|      * @param player Player instance. | ||||
|      */ | ||||
|     protected fixVideoJSPlayerSize(player: VideoJSPlayer): void { | ||||
|         const videoWidth = player.videoWidth(); | ||||
|         const videoHeight = player.videoHeight(); | ||||
|         const playerDimensions = player.currentDimensions(); | ||||
|         if (!videoWidth || !videoHeight || !playerDimensions.width || videoWidth === playerDimensions.width) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const candidateHeight = playerDimensions.width * videoHeight / videoWidth; | ||||
|         if (!playerDimensions.height || Math.abs(candidateHeight - playerDimensions.height) > 1) { | ||||
|             player.dimension('height', candidateHeight); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const AddonFilterMediaPluginVideoJS = makeSingleton(AddonFilterMediaPluginVideoJSService); | ||||
| 
 | ||||
| /** | ||||
|  * Data passed to VIDEO_JS_PLAYER_CREATED event. | ||||
|  */ | ||||
| export type CoreEventJSVideoPlayerCreated = { | ||||
|     element: HTMLAudioElement | HTMLVideoElement; | ||||
|     player: VideoJSPlayer; | ||||
| }; | ||||
							
								
								
									
										28
									
								
								src/addons/filter/mediaplugin/utils/videojs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/addons/filter/mediaplugin/utils/videojs.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| // (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 { VideoJSOgvJS } from '@addons/filter/mediaplugin/classes/videojs-ogvjs'; | ||||
| import { OGVLoader } from 'ogv'; | ||||
| import videojs from 'video.js'; | ||||
| 
 | ||||
| export { videojs }; | ||||
| 
 | ||||
| /** | ||||
|  * Initialize the controller. | ||||
|  */ | ||||
| export function initializeVideoJSOgvJS(): void { | ||||
|     OGVLoader.base = 'assets/lib/ogv'; | ||||
| 
 | ||||
|     videojs.getComponent('Tech').registerTech('OgvJS', VideoJSOgvJS); | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'addon.messages.groupinfo' | translate }}</h2> | ||||
|             <h1>{{ 'addon.messages.groupinfo' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
| @ -19,18 +19,18 @@ | ||||
|         <ion-item class="ion-text-center" *ngIf="conversation"> | ||||
|             <ion-label> | ||||
|                 <div class="large-avatar"> | ||||
|                     <img class="avatar" [src]="conversation!.imageurl" core-external-content [alt]="conversation!.name" | ||||
|                     <img class="avatar" [src]="conversation.imageurl" core-external-content [alt]="conversation.name" | ||||
|                         onError="this.src='assets/img/group-avatar.svg'"> | ||||
|                 </div> | ||||
|                 <h2> | ||||
|                     <core-format-text [text]="conversation!.name" contextLevel="system" [contextInstanceId]="0"></core-format-text> | ||||
|                     <core-format-text [text]="conversation.name" contextLevel="system" [contextInstanceId]="0"></core-format-text> | ||||
|                 </h2> | ||||
|                 <p> | ||||
|                     <core-format-text *ngIf="conversation!.subname" [text]="conversation!.subname" contextLevel="system" | ||||
|                     <core-format-text *ngIf="conversation.subname" [text]="conversation.subname" contextLevel="system" | ||||
|                         [contextInstanceId]="0"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|                 <p>{{ 'addon.messages.numparticipants' | translate:{$a: conversation!.membercount} }}</p> | ||||
|                 <p>{{ 'addon.messages.numparticipants' | translate:{$a: conversation.membercount} }}</p> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|  | ||||
| @ -7,6 +7,8 @@ | ||||
|             <h1>{{ 'addon.messages.contacts' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||
|             <core-context-menu></core-context-menu> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { | ||||
| @ -29,6 +29,7 @@ import { ActivatedRoute } from '@angular/router'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of contacts. | ||||
| @ -40,6 +41,8 @@ import { CoreNavigator } from '@services/navigator'; | ||||
| }) | ||||
| export class AddonMessagesContacts35Page implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     protected searchingMessages: string; | ||||
|     protected loadingMessages: string; | ||||
|     protected siteId: string; | ||||
| @ -244,7 +247,9 @@ export class AddonMessagesContacts35Page implements OnInit, OnDestroy { | ||||
|         const path = CoreNavigator.getRelativePathToParent('/messages/contacts-35') + `discussion/user/${discussionUserId}`; | ||||
| 
 | ||||
|         // @todo Check why this is failing on ngInit.
 | ||||
|         CoreNavigator.navigate(path); | ||||
|         CoreNavigator.navigate(path, { | ||||
|             reset: CoreScreen.isTablet && !!this.splitView && !this.splitView.isNested, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -10,6 +10,8 @@ | ||||
|             <ion-button fill="clear" (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate"> | ||||
|                 <ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||
|             <core-context-menu></core-context-menu> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { | ||||
| @ -24,6 +24,7 @@ import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays contacts and contact requests. | ||||
| @ -37,6 +38,8 @@ import { IonRefresher } from '@ionic/angular'; | ||||
| }) | ||||
| export class AddonMessagesContactsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     selected: 'confirmed' | 'requests' = 'confirmed'; | ||||
|     requestsBadge = ''; | ||||
|     selectedUserId?: number; // User id of the conversation opened in the split view.
 | ||||
| @ -292,7 +295,9 @@ export class AddonMessagesContactsPage implements OnInit, OnDestroy { | ||||
|         this.selectedUserId = userId; | ||||
| 
 | ||||
|         const path = CoreNavigator.getRelativePathToParent('/messages/contacts') + `discussion/user/${userId}`; | ||||
|         CoreNavigator.navigate(path); | ||||
|         CoreNavigator.navigate(path, { | ||||
|             reset: CoreScreen.isTablet && !!this.splitView && !this.splitView.isNested, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -7,6 +7,8 @@ | ||||
|             <h1>{{ 'addon.messages.messages' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||
|             <core-context-menu></core-context-menu> | ||||
|             <core-user-menu-button></core-user-menu-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| @ -29,7 +31,7 @@ | ||||
|                     [attr.aria-label]="'addon.messages.contacts' | translate" detail="true" button> | ||||
|                     <ion-icon name="fas-address-book" slot="start" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.messages.contacts' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.messages.contacts' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { | ||||
| @ -34,6 +34,7 @@ import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; | ||||
| import { CorePlatform } from '@services/platform'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of discussions. | ||||
| @ -45,6 +46,8 @@ import { CorePlatform } from '@services/platform'; | ||||
| }) | ||||
| export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     protected newMessagesObserver: CoreEventObserver; | ||||
|     protected readChangedObserver: CoreEventObserver; | ||||
|     protected appResumeSubscription: Subscription; | ||||
| @ -264,7 +267,10 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         const path = CoreNavigator.getRelativePathToParent('/messages/index') + `discussion/user/${discussionUserId}`; | ||||
| 
 | ||||
|         await CoreNavigator.navigate(path, { params }); | ||||
|         await CoreNavigator.navigate(path, { | ||||
|             params, | ||||
|             reset: CoreScreen.isTablet && !!this.splitView && !this.splitView.isNested, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -13,6 +13,8 @@ | ||||
|             <ion-button (click)="gotoSettings()" [attr.aria-label]="'addon.messages.messagepreferences' | translate"> | ||||
|                 <ion-icon name="fas-cog" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||
|             <core-context-menu></core-context-menu> | ||||
|             <core-user-menu-button></core-user-menu-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
|  | ||||
| @ -38,6 +38,7 @@ import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; | ||||
| import { CorePlatform } from '@services/platform'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of conversations, including group conversations. | ||||
| @ -49,6 +50,8 @@ import { CorePlatform } from '@services/platform'; | ||||
| }) | ||||
| export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     @ViewChild(IonContent) content?: IonContent; | ||||
|     @ViewChild('favlist') favListEl?: ElementRef; | ||||
|     @ViewChild('grouplist') groupListEl?: ElementRef; | ||||
| @ -526,7 +529,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|         const path = CoreNavigator.getRelativePathToParent('/messages/group-conversations') + 'discussion/' + | ||||
|             (conversationId ? conversationId : `user/${userId}`); | ||||
| 
 | ||||
|         await CoreNavigator.navigate(path, { params }); | ||||
|         await CoreNavigator.navigate(path, { | ||||
|             params, | ||||
|             reset: CoreScreen.isTablet && !!this.splitView && !this.splitView.isNested, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -7,6 +7,8 @@ | ||||
|             <h1>{{ 'addon.messages.searchcombined' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||
|             <core-context-menu></core-context-menu> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnDestroy } from '@angular/core'; | ||||
| import { Component, OnDestroy, ViewChild } from '@angular/core'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { | ||||
| @ -25,6 +25,7 @@ import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| 
 | ||||
| /** | ||||
|  * Page for searching users. | ||||
| @ -35,6 +36,8 @@ import { CoreScreen } from '@services/screen'; | ||||
| }) | ||||
| export class AddonMessagesSearchPage implements OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     disableSearch = false; | ||||
|     displaySearching = false; | ||||
|     displayResults = false; | ||||
| @ -260,7 +263,9 @@ export class AddonMessagesSearchPage implements OnDestroy { | ||||
|             const path = CoreNavigator.getRelativePathToParent('/messages/search') + 'discussion/' + | ||||
|                 (conversationId ? conversationId : `user/${userId}`); | ||||
| 
 | ||||
|             CoreNavigator.navigate(path); | ||||
|             CoreNavigator.navigate(path, { | ||||
|                 reset: CoreScreen.isTablet && !!this.splitView && !this.splitView.isNested, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ plugin.name }}</h2> | ||||
|             <h1>{{ plugin.name }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -25,7 +25,7 @@ | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.course.hiddenfromstudents' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'core.course.hiddenfromstudents' | translate }}</p> | ||||
|                     <p *ngIf="module.visible">{{ 'core.no' | translate }}</p> | ||||
|                     <p *ngIf="!module.visible">{{ 'core.yes' | translate }}</p> | ||||
|                 </ion-label> | ||||
| @ -33,13 +33,13 @@ | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap" *ngIf="timeRemaining"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_assign.timeremaining' | translate }}</p> | ||||
|                     <p>{{ timeRemaining }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="lateSubmissions"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_assign.latesubmissions' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_assign.latesubmissions' | translate }}</p> | ||||
|                     <p>{{ lateSubmissions }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
| @ -47,8 +47,8 @@ | ||||
|             <!-- Summary of all submissions. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="summary && summary.participantcount" (click)="goToSubmissionList()" detail="true" button> | ||||
|                 <ion-label> | ||||
|                     <h2 *ngIf="assign.teamsubmission">{{ 'addon.mod_assign.numberofteams' | translate }}</h2> | ||||
|                     <h2 *ngIf="!assign.teamsubmission">{{ 'addon.mod_assign.numberofparticipants' | translate }}</h2> | ||||
|                     <p class="item-heading" *ngIf="assign.teamsubmission">{{ 'addon.mod_assign.numberofteams' | translate }}</p> | ||||
|                     <p class="item-heading" *ngIf="!assign.teamsubmission">{{ 'addon.mod_assign.numberofparticipants' | translate }}</p> | ||||
|                 </ion-label> | ||||
|                 <ion-badge slot="end" color="primary"> | ||||
|                     <span aria-hidden="true">{{ summary.participantcount }}</span> | ||||
| @ -66,7 +66,7 @@ | ||||
|                 [class.hide-detail]="!summary.submissiondraftscount" [detail]="true" [button]="summary.submissiondraftscount" | ||||
|                 (click)="goToSubmissionList(submissionStatusDraft, !!summary.submissiondraftscount)"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</p> | ||||
|                 </ion-label> | ||||
|                 <ion-badge slot="end" color="primary"> | ||||
|                     <span aria-hidden="true">{{ summary.submissiondraftscount }}</span> | ||||
| @ -82,7 +82,7 @@ | ||||
|                 [class.hide-detail]="!summary.submissionssubmittedcount" [detail]="true" [button]="summary.submissionssubmittedcount" | ||||
|                 (click)="goToSubmissionList(submissionStatusSubmitted, !!summary.submissionssubmittedcount)"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</p> | ||||
|                 </ion-label> | ||||
|                 <ion-badge slot="end" color="primary"> | ||||
|                     <span aria-hidden="true">{{ summary.submissionssubmittedcount }}</span> | ||||
| @ -98,7 +98,7 @@ | ||||
|                 [class.hide-detail]="!needsGradingAvailable" [detail]="true" [button]="needsGradingAvailable" | ||||
|                 (click)="goToSubmissionList(needGrading, needsGradingAvailable)"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}</p> | ||||
|                 </ion-label> | ||||
|                 <ion-badge slot="end" color="primary"> | ||||
|                     <span aria-hidden="true">{{ summary.submissionsneedgradingcount }}</span> | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
|             [attr.aria-label]="user!.fullname"> | ||||
|             <core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar> | ||||
|             <ion-label> | ||||
|                 <h2>{{ user!.fullname }}</h2> | ||||
|                 <p class="item-heading">{{ user!.fullname }}</p> | ||||
|                 <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| @ -23,7 +23,7 @@ | ||||
|         <!-- Status of the submission if user is blinded. --> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="blindMarking && !user"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2> | ||||
|                 <p class="item-heading">{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</p> | ||||
|                 <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| @ -31,7 +31,7 @@ | ||||
|         <!-- Status of the submission in the rest of cases. --> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2> | ||||
|                 <p class="item-heading">{{ 'addon.mod_assign.submissionstatus' | translate }}</p> | ||||
|                 <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| @ -44,7 +44,7 @@ | ||||
|                     <!-- Render some data about the submission. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.attemptnumber' | translate }}</p> | ||||
|                             <p *ngIf="assign!.maxattempts == unlimitedAttempts"> | ||||
|                                 {{ 'addon.mod_assign.outof' | translate : | ||||
|                                 {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||
| @ -59,7 +59,7 @@ | ||||
|                     <!-- Submission is locked. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.submissionslocked' | translate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| 
 | ||||
| @ -77,7 +77,7 @@ | ||||
| 
 | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="showDates && assign!.duedate && !isSubmittedForGrading"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.duedate' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.duedate' | translate }}</p> | ||||
|                             <p *ngIf="assign!.duedate">{{ assign!.duedate * 1000 | coreFormatDate }}</p> | ||||
|                             <p *ngIf="!assign!.duedate">{{ 'addon.mod_assign.duedateno' | translate }}</p> | ||||
|                         </ion-label> | ||||
| @ -85,14 +85,14 @@ | ||||
| 
 | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && assign!.cutoffdate && isSubmittedForGrading"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.cutoffdate' | translate }}</p> | ||||
|                             <p>{{ assign!.cutoffdate * 1000 | coreFormatDate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| 
 | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.extensionduedate' | translate }}</p> | ||||
|                             <p>{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| @ -100,7 +100,7 @@ | ||||
|                     <!-- Time remaining. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="timeRemaining || timeLimitEndTime > 0" [ngClass]="[timeRemainingClass]"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.timeremaining' | translate }}</p> | ||||
|                             <p *ngIf="!timeLimitEndTime" [innerHTML]="timeRemaining"></p> | ||||
|                             <core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" mode="basic" timeUpText="00:00:00" | ||||
|                                 [timeLeftClassThreshold]="-1" [underTimeClassThresholds]="[300, 900]" (finished)="timeUp()"> | ||||
| @ -111,7 +111,7 @@ | ||||
|                     <!-- Time limit. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="assign && assign.timelimit"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.timelimit' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.timelimit' | translate }}</p> | ||||
|                             <p>{{ assign.timelimit | coreDuration }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| @ -120,7 +120,7 @@ | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined" | ||||
|                         [ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.editingstatus' | translate }}</p> | ||||
|                             <p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p> | ||||
|                             <p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p> | ||||
|                         </ion-label> | ||||
| @ -130,7 +130,7 @@ | ||||
|                     <ion-item class="ion-text-wrap" | ||||
|                         *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.timemodified' | translate }}</p> | ||||
|                             <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| @ -151,7 +151,7 @@ | ||||
|                                 [attr.aria-label]="user.fullname"> | ||||
|                                 <core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar> | ||||
|                                 <ion-label> | ||||
|                                     <h2>{{ user.fullname }}</h2> | ||||
|                                     <p class="item-heading">{{ user.fullname }}</p> | ||||
|                                 </ion-label> | ||||
|                             </ion-item> | ||||
|                         </ng-container> | ||||
| @ -257,7 +257,7 @@ | ||||
|                     <ion-item class="ion-text-wrap core-grading-summary" | ||||
|                         *ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.currentgrade' | translate }}</p> | ||||
|                             <p> | ||||
|                                 <core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text> | ||||
|                             </p> | ||||
| @ -273,7 +273,7 @@ | ||||
|                         Use a text input because otherwise we cannot readthe value if it has an invalid character. --> | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && !grade.scale"> | ||||
|                             <ion-label position="stacked"> | ||||
|                                 <h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2> | ||||
|                                 <p class="item-heading">{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</p> | ||||
|                             </ion-label> | ||||
|                             <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade" | ||||
|                                 [lang]="grade.lang"> | ||||
| @ -284,7 +284,7 @@ | ||||
|                         <!-- Grade using a scale. --> | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && grade.scale"> | ||||
|                             <ion-label> | ||||
|                                 <h2>{{ 'addon.mod_assign.grade' | translate }}</h2> | ||||
|                                 <p class="item-heading">{{ 'addon.mod_assign.grade' | translate }}</p> | ||||
|                             </ion-label> | ||||
|                             <ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled" | ||||
|                                 [interfaceOptions]="{header: 'addon.mod_assign.grade' | translate}"> | ||||
| @ -297,7 +297,7 @@ | ||||
|                         <!-- Outcomes. --> | ||||
|                         <ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes"> | ||||
|                             <ion-label> | ||||
|                                 <h2>{{ outcome.name }}</h2> | ||||
|                                 <p class="item-heading">{{ outcome.name }}</p> | ||||
|                             </ion-label> | ||||
|                             <ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" | ||||
|                                 interface="action-sheet" [disabled]="gradeInfo!.disabled" [interfaceOptions]="{header: outcome.name }"> | ||||
| @ -311,7 +311,7 @@ | ||||
|                         <!-- Gradebook grade for simple grading. --> | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'"> | ||||
|                             <ion-label> | ||||
|                                 <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> | ||||
|                                 <p class="item-heading">{{ 'addon.mod_assign.currentgrade' | translate }}</p> | ||||
|                                 <p *ngIf="grade.gradebookGrade && !grade.scale"> | ||||
|                                     {{ grade.gradebookGrade }} | ||||
|                                 </p> | ||||
| @ -332,7 +332,7 @@ | ||||
|                     <!-- Workflow status. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="workflowStatusTranslationId"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.markingworkflowstate' | translate }}</p> | ||||
|                             <p>{{ workflowStatusTranslationId | translate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| @ -340,7 +340,7 @@ | ||||
|                     <!--- Apply grade to all team members. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</p> | ||||
|                             <p>{{ 'addon.mod_assign.applytoteam' | translate }}</p> | ||||
|                         </ion-label> | ||||
|                         <ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle> | ||||
| @ -350,7 +350,7 @@ | ||||
|                     <ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone"> | ||||
|                         <ion-item class="ion-text-wrap"> | ||||
|                             <ion-label> | ||||
|                                 <h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2> | ||||
|                                 <p class="item-heading">{{ 'addon.mod_assign.attemptsettings' | translate }}</p> | ||||
|                                 <p *ngIf="assign!.maxattempts == unlimitedAttempts"> | ||||
|                                     {{ 'addon.mod_assign.outof' | translate : | ||||
|                                     {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||
| @ -376,8 +376,8 @@ | ||||
|                         [attr.aria-label]="grader!.fullname" detail="true"> | ||||
|                         <core-user-avatar [user]="grader" slot="start" [linkProfile]="false"></core-user-avatar> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2> | ||||
|                             <h2>{{ grader!.fullname }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.gradedby' | translate }}</p> | ||||
|                             <p class="item-heading">{{ grader!.fullname }}</p> | ||||
|                             <p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| @ -385,7 +385,7 @@ | ||||
|                     <!-- Grader is hidden, display only the grade date. --> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="!grader && feedback?.gradeddate"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_assign.gradedon' | translate }}</p> | ||||
|                             <p>{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|  | ||||
| @ -20,7 +20,7 @@ | ||||
|     </ion-item-divider> | ||||
|     <ion-item class="ion-text-wrap" *ngIf="wordLimitEnabled && words >= 0"> | ||||
|         <ion-label> | ||||
|             <h2>{{ 'addon.mod_assign.wordlimit' | translate }}</h2> | ||||
|             <p class="item-heading">{{ 'addon.mod_assign.wordlimit' | translate }}</p> | ||||
|             <p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + wordLimit} }}</p> | ||||
|         </ion-label> | ||||
|     </ion-item> | ||||
|  | ||||
| @ -19,13 +19,13 @@ | ||||
|     <ng-container *ngIf="meetingInfo && showRoom"> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="meetingInfo.openingtime"> | ||||
|             <ion-label> | ||||
|                 <h3>{{ 'addon.mod_bigbluebuttonbn.mod_form_field_openingtime' | translate }}</h3> | ||||
|                 <p class="item-heading">{{ 'addon.mod_bigbluebuttonbn.mod_form_field_openingtime' | translate }}</p> | ||||
|             </ion-label> | ||||
|             <p slot="end">{{ meetingInfo.openingtime * 1000 | coreFormatDate }}</p> | ||||
|         </ion-item> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="meetingInfo.closingtime"> | ||||
|             <ion-label> | ||||
|                 <h3>{{ 'addon.mod_bigbluebuttonbn.mod_form_field_closingtime' | translate }}</h3> | ||||
|                 <p class="item-heading">{{ 'addon.mod_bigbluebuttonbn.mod_form_field_closingtime' | translate }}</p> | ||||
|             </ion-label> | ||||
|             <p slot="end">{{ meetingInfo.closingtime * 1000 | coreFormatDate }}</p> | ||||
|         </ion-item> | ||||
| @ -45,31 +45,31 @@ | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap" *ngIf="meetingInfo.startedat"> | ||||
|                 <ion-label> | ||||
|                     <h3>{{ 'addon.mod_bigbluebuttonbn.view_message_session_started_at' | translate }}</h3> | ||||
|                     <p class="item-heading">{{ 'addon.mod_bigbluebuttonbn.view_message_session_started_at' | translate }}</p> | ||||
|                 </ion-label> | ||||
|                 <p slot="end">{{ meetingInfo.startedat * 1000 | coreFormatDate: "strftimetime" }}</p> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h3 *ngIf="meetingInfo.moderatorplural"> | ||||
|                     <p class="item-heading" *ngIf="meetingInfo.moderatorplural"> | ||||
|                         {{ 'addon.mod_bigbluebuttonbn.view_message_moderators' | translate }} | ||||
|                     </h3> | ||||
|                     <h3 *ngIf="!meetingInfo.moderatorplural"> | ||||
|                     </p> | ||||
|                     <p class="item-heading" *ngIf="!meetingInfo.moderatorplural"> | ||||
|                         {{ 'addon.mod_bigbluebuttonbn.view_message_moderator' | translate }} | ||||
|                     </h3> | ||||
|                     </p> | ||||
|                 </ion-label> | ||||
|                 <p slot="end">{{ meetingInfo.moderatorcount }}</p> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h3 *ngIf="meetingInfo.participantplural"> | ||||
|                     <p class="item-heading" *ngIf="meetingInfo.participantplural"> | ||||
|                         {{ 'addon.mod_bigbluebuttonbn.view_message_viewers' | translate }} | ||||
|                     </h3> | ||||
|                     <h3 *ngIf="!meetingInfo.participantplural"> | ||||
|                     </p> | ||||
|                     <p class="item-heading" *ngIf="!meetingInfo.participantplural"> | ||||
|                         {{ 'addon.mod_bigbluebuttonbn.view_message_viewer' | translate }} | ||||
|                     </h3> | ||||
|                     </p> | ||||
|                 </ion-label> | ||||
|                 <p slot="end">{{ meetingInfo.participantcount }}</p> | ||||
|             </ion-item> | ||||
| @ -108,7 +108,7 @@ | ||||
|             <div [hidden]="!recording.expanded" class="addon-mod_bbb-recording-details"> | ||||
|                 <ion-item *ngFor="let data of recording.details" class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ data.label }}</h2> | ||||
|                         <p class="item-heading">{{ data.label }}</p> | ||||
|                         <p *ngIf="data.allowHTML"> | ||||
|                             <core-format-text [text]="data.value" [component]="component" [componentId]="module.id" contextLevel="module" | ||||
|                                 [contextInstanceId]="module.id" [courseId]="module.course"></core-format-text> | ||||
|  | ||||
| @ -24,8 +24,8 @@ | ||||
|             (click)="openBook(chapter.id)"> | ||||
|             <ion-label> | ||||
|                 <p [class.ion-padding-start]="addPadding && chapter.level == 1 ? true : null"> | ||||
|                     <span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.indexNumber}} </span> | ||||
|                     <span *ngIf="showBullets" class="addon-mod-book-bullet">• </span> | ||||
|                     <span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.indexNumber}} </span> | ||||
|                     <span *ngIf="showBullets" class="addon-mod-book-bullet">• </span> | ||||
|                     <core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { AddonModBook, AddonModBookBookWSData, AddonModBookNumbering, AddonModBo | ||||
| import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { AddonModBookModuleHandlerService } from '../../services/handlers/module'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays a book entry page. | ||||
| @ -60,6 +61,13 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async invalidateContent(): Promise<void> { | ||||
|         await AddonModBook.invalidateContent(this.module.id, this.courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load book data. | ||||
|      * | ||||
| @ -102,14 +110,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | ||||
|      * | ||||
|      * @param chapterId Chapter to open, undefined for last chapter viewed. | ||||
|      */ | ||||
|     openBook(chapterId?: number): void { | ||||
|         CoreNavigator.navigate('contents', { | ||||
|             params: { | ||||
|                 cmId: this.module.id, | ||||
|                 courseId: this.courseId, | ||||
|                 chapterId, | ||||
|             }, | ||||
|         }); | ||||
|     async openBook(chapterId?: number): Promise<void> { | ||||
|         await CoreNavigator.navigateToSitePath( | ||||
|             `${AddonModBookModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/contents`, | ||||
|             { params: { chapterId } }, | ||||
|         ); | ||||
| 
 | ||||
|         this.hasStartedBook = true; | ||||
|     } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'addon.mod_book.toc' | translate }}</h2> | ||||
|             <h1>{{ 'addon.mod_book.toc' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
| @ -17,8 +17,8 @@ | ||||
|                 [attr.aria-current]="selected == chapter.id ? 'page' : 'false'" button [class.item-dimmed]="chapter.hidden" detail="false"> | ||||
|                 <ion-label> | ||||
|                     <p [class.ion-padding-start]="addPadding && chapter.level == 1 ? true : null" class="item-heading"> | ||||
|                         <span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.indexNumber}} </span> | ||||
|                         <span *ngIf="showBullets" class="addon-mod-book-bullet">• </span> | ||||
|                         <span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.indexNumber}} </span> | ||||
|                         <span *ngIf="showBullets" class="addon-mod-book-bullet">• </span> | ||||
|                         <core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"> | ||||
|                         </core-format-text> | ||||
|                     </p> | ||||
|  | ||||
| @ -291,7 +291,9 @@ export class AddonModBookProvider { | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             chapterNumber++; | ||||
|             if (!parseInt(chapter.hidden, 10)) { | ||||
|                 chapterNumber++; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         return chapters; | ||||
|  | ||||
							
								
								
									
										308
									
								
								src/addons/mod/book/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										308
									
								
								src/addons/mod/book/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,308 @@ | ||||
| @mod @mod_book @app @javascript | ||||
| Feature: Test basic usage of book activity in app | ||||
|   In order to view a book while using the mobile app | ||||
|   As a student | ||||
|   I need basic book functionality to work | ||||
| 
 | ||||
|   Background: | ||||
|     Given the following "users" exist: | ||||
|       | username | firstname | lastname | email                | | ||||
|       | teacher1 | Teacher   | teacher  | teacher1@example.com | | ||||
|       | student1 | Student   | student  | student1@example.com | | ||||
|     And the following "courses" exist: | ||||
|       | fullname | shortname | category | | ||||
|       | Course 1 | C1        | 0        | | ||||
|     And the following "course enrolments" exist: | ||||
|       | user     | course | role           | | ||||
|       | teacher1 | C1     | editingteacher | | ||||
|       | student1 | C1     | student        | | ||||
|     And the following "activities" exist: | ||||
|       | activity | name       | intro                 | course | idnumber | numbering | | ||||
|       | book     | Basic book | Test book description | C1     | book     | 1         | | ||||
|     And the following "mod_book > chapter" exist: | ||||
|       | book       | title             | content                     | subchapter | hidden | pagenum | | ||||
|       | Basic book | Chapt 1           | This is the first chapter   | 0          | 0      | 1       | | ||||
|       | Basic book | Chapt 1.1         | This is a subchapter        | 1          | 0      | 2       | | ||||
|       | Basic book | Chapt 2           | This is the second chapter  | 0          | 0      | 3       | | ||||
|       | Basic book | Hidden chapter    | This is a hidden chapter    | 0          | 1      | 4       | | ||||
|       | Basic book | Hidden subchapter | This is a hidden subchapter | 1          | 1      | 5       | | ||||
|       | Basic book | Chapt 3           | This is the third chapter   | 0          | 0      | 6       | | ||||
|       | Basic book | Last hidden       | Another hidden subchapter   | 1          | 1      | 7       | | ||||
| 
 | ||||
|   Scenario: View book table of contents (student) | ||||
|     Given I entered the course "Course 1" as "student1" in the app | ||||
|     And I press "Basic book" in the app | ||||
|     Then I should find "Test book description" in the app | ||||
|     And I should find "Chapt 1" in the app | ||||
|     And I should find "Chapt 1.1" in the app | ||||
|     And I should find "Chapt 2" in the app | ||||
|     And I should find "Chapt 3" in the app | ||||
|     And I should find "Start" in the app | ||||
|     But I should not find "Hidden chapter" in the app | ||||
|     And I should not find "Hidden subchapter" in the app | ||||
|     And I should not find "Last hidden" in the app | ||||
|     And I should not find "This is the first chapter" in the app | ||||
| 
 | ||||
|     When I press "Start" in the app | ||||
|     And I press "Table of contents" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "Chapt 1.1" in the app | ||||
|     And I should find "Chapt 2" in the app | ||||
|     And I should find "Chapt 3" in the app | ||||
|     But I should not find "Hidden chapter" in the app | ||||
|     And I should not find "Hidden subchapter" in the app | ||||
|     And I should not find "Last hidden" in the app | ||||
| 
 | ||||
|   Scenario: View book table of contents (teacher) | ||||
|     Given I entered the course "Course 1" as "teacher1" in the app | ||||
|     And I press "Basic book" in the app | ||||
|     Then I should find "Test book description" in the app | ||||
|     And I should find "Chapt 1" in the app | ||||
|     And I should find "Chapt 1.1" in the app | ||||
|     And I should find "Chapt 2" in the app | ||||
|     And I should find "Hidden chapter" in the app | ||||
|     And I should find "Hidden subchapter" in the app | ||||
|     And I should find "Chapt 3" in the app | ||||
|     And I should find "Last hidden" in the app | ||||
|     And I should find "Start" in the app | ||||
|     And I should not find "This is the first chapter" in the app | ||||
| 
 | ||||
|     When I press "Start" in the app | ||||
|     And I press "Table of contents" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "Chapt 1.1" in the app | ||||
|     And I should find "Chapt 2" in the app | ||||
|     And I should find "Hidden chapter" in the app | ||||
|     And I should find "Hidden subchapter" in the app | ||||
|     And I should find "Chapt 3" in the app | ||||
|     And I should find "Last hidden" in the app | ||||
| 
 | ||||
|   Scenario: Open chapters from table of contents | ||||
|     Given I entered the course "Course 1" as "student1" in the app | ||||
|     And I press "Basic book" in the app | ||||
|     When I press "Chapt 1" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "This is the first chapter" in the app | ||||
|     But I should not find "This is the second chapter" in the app | ||||
| 
 | ||||
|     When I press the back button in the app | ||||
|     And I press "Chapt 2" in the app | ||||
|     Then I should find "Chapt 2" in the app | ||||
|     And I should find "This is the second chapter" in the app | ||||
|     But I should not find "This is the first chapter" in the app | ||||
| 
 | ||||
|   Scenario: View and navigate book contents (student) | ||||
|     Given I entered the course "Course 1" as "student1" in the app | ||||
|     And I press "Basic book" in the app | ||||
|     And I press "Start" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "This is the first chapter" in the app | ||||
|     And I should find "1 / 4" in the app | ||||
| 
 | ||||
|     When I press "Next" in the app | ||||
|     Then I should find "Chapt 1.1" in the app | ||||
|     And I should find "This is a subchapter" in the app | ||||
|     And I should find "2 / 4" in the app | ||||
|     But I should not find "This is the first chapter" in the app | ||||
| 
 | ||||
|     When I press "Next" in the app | ||||
|     Then I should find "Chapt 2" in the app | ||||
|     And I should find "This is the second chapter" in the app | ||||
|     And I should find "3 / 4" in the app | ||||
|     But I should not find "This is a subchapter" in the app | ||||
| 
 | ||||
|     When I press "Previous" in the app | ||||
|     Then I should find "Chapt 1.1" in the app | ||||
|     And I should find "This is a subchapter" in the app | ||||
|     And I should find "2 / 4" in the app | ||||
|     But I should not find "This is the second chapter" in the app | ||||
| 
 | ||||
|     # Navigate using TOC. | ||||
|     When I press "Table of contents" in the app | ||||
|     And I press "Chapt 1" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "This is the first chapter" in the app | ||||
|     And I should find "1 / 4" in the app | ||||
|     But I should not find "This is a subchapter" in the app | ||||
| 
 | ||||
|     When I press "Table of contents" in the app | ||||
|     And I press "Chapt 3" in the app | ||||
|     Then I should find "Chapt 3" in the app | ||||
|     And I should find "This is the third chapter" in the app | ||||
|     And I should find "4 / 4" in the app | ||||
|     But I should not find "This is the first chapter" in the app | ||||
| 
 | ||||
|     # Navigate using swipe. | ||||
|     When I swipe to the left in "Chapt 3" "ion-slides" in the app | ||||
|     Then I should find "Chapt 3" in the app | ||||
|     And I should find "This is the third chapter" in the app | ||||
|     And I should find "4 / 4" in the app | ||||
| 
 | ||||
|     When I swipe to the right in "Chapt 3" "ion-slides" in the app | ||||
|     Then I should find "Chapt 2" in the app | ||||
|     And I should find "This is the second chapter" in the app | ||||
|     And I should find "3 / 4" in the app | ||||
| 
 | ||||
|     When I swipe to the right in "Chapt 2" "ion-slides" in the app | ||||
|     Then I should find "Chapt 1.1" in the app | ||||
|     And I should find "This is a subchapter" in the app | ||||
|     And I should find "2 / 4" in the app | ||||
| 
 | ||||
|     When I swipe to the left in "Chapt 1.1" "ion-slides" in the app | ||||
|     Then I should find "Chapt 2" in the app | ||||
|     And I should find "This is the second chapter" in the app | ||||
|     And I should find "3 / 4" in the app | ||||
| 
 | ||||
| Scenario: View and navigate book contents (teacher) | ||||
|     Given I entered the course "Course 1" as "teacher1" in the app | ||||
|     And I press "Basic book" in the app | ||||
|     And I press "Start" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "This is the first chapter" in the app | ||||
|     And I should find "1 / 7" in the app | ||||
| 
 | ||||
|     When I press "Next" in the app | ||||
|     Then I should find "Chapt 1.1" in the app | ||||
|     And I should find "This is a subchapter" in the app | ||||
|     And I should find "2 / 7" in the app | ||||
|     But I should not find "This is the first chapter" in the app | ||||
| 
 | ||||
|     When I press "Next" in the app | ||||
|     Then I should find "Chapt 2" in the app | ||||
|     And I should find "This is the second chapter" in the app | ||||
|     And I should find "3 / 7" in the app | ||||
|     But I should not find "This is a subchapter" in the app | ||||
| 
 | ||||
|     When I press "Next" in the app | ||||
|     Then I should find "Hidden chapter" in the app | ||||
|     And I should find "This is a hidden chapter" in the app | ||||
|     And I should find "4 / 7" in the app | ||||
|     But I should not find "This is the second chapter" in the app | ||||
| 
 | ||||
|     When I press "Next" in the app | ||||
|     Then I should find "Hidden subchapter" in the app | ||||
|     And I should find "This is a hidden subchapter" in the app | ||||
|     And I should find "5 / 7" in the app | ||||
|     But I should not find "This is a hidden chapter" in the app | ||||
| 
 | ||||
|     When I press "Previous" in the app | ||||
|     Then I should find "Hidden chapter" in the app | ||||
|     And I should find "This is a hidden chapter" in the app | ||||
|     And I should find "4 / 7" in the app | ||||
|     But I should not find "This is a hidden subchapter" in the app | ||||
| 
 | ||||
|     # Navigate using TOC. | ||||
|     When I press "Table of contents" in the app | ||||
|     And I press "Chapt 1" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "This is the first chapter" in the app | ||||
|     And I should find "1 / 7" in the app | ||||
|     But I should not find "This is a hidden chapter" in the app | ||||
| 
 | ||||
|     When I press "Table of contents" in the app | ||||
|     And I press "Hidden subchapter" in the app | ||||
|     Then I should find "Hidden subchapter" in the app | ||||
|     And I should find "This is a hidden subchapter" in the app | ||||
|     And I should find "5 / 7" in the app | ||||
|     But I should not find "This is the first chapter" in the app | ||||
| 
 | ||||
|     # Navigate using swipe. | ||||
|     When I swipe to the left in "Hidden subchapter" "ion-slides" in the app | ||||
|     Then I should find "Chapt 3" in the app | ||||
|     And I should find "This is the third chapter" in the app | ||||
|     And I should find "6 / 7" in the app | ||||
| 
 | ||||
|     When I swipe to the left in "Chapt 3" "ion-slides" in the app | ||||
|     Then I should find "Last hidden" in the app | ||||
|     And I should find "Another hidden subchapter" in the app | ||||
|     And I should find "7 / 7" in the app | ||||
| 
 | ||||
|     When I swipe to the left in "Last hidden" "ion-slides" in the app | ||||
|     Then I should find "Last hidden" in the app | ||||
|     And I should find "Another hidden subchapter" in the app | ||||
|     And I should find "7 / 7" in the app | ||||
| 
 | ||||
|     When I swipe to the right in "Last hidden" "ion-slides" in the app | ||||
|     Then I should find "Chapt 3" in the app | ||||
|     And I should find "This is the third chapter" in the app | ||||
|     And I should find "6 / 7" in the app | ||||
| 
 | ||||
|   Scenario: Link to book opens chapter content | ||||
|     Given I entered the book activity "Basic book" on course "Course 1" as "student1" in the app | ||||
|     Then I should find "This is the first chapter" in the app | ||||
| 
 | ||||
|   Scenario: Test numbering (student) | ||||
|     Given the following "activities" exist: | ||||
|       | activity | name      | intro                 | course | idnumber  | numbering | | ||||
|       | book     | Bull book | Test book description | C1     | book2     | 2         | | ||||
|       | book     | Ind book  | Test book description | C1     | book2     | 3         | | ||||
|       | book     | None book | Test book description | C1     | book2     | 0         | | ||||
|     And the following "mod_book > chapter" exist: | ||||
|       | book      | title             | content                     | subchapter | hidden | pagenum | | ||||
|       | Bull book | Chapt 1           | This is the first chapter   | 0          | 0      | 1       | | ||||
|       | Ind book  | Chapt 1           | This is the first chapter   | 0          | 0      | 1       | | ||||
|       | None book | Chapt 1           | This is the first chapter   | 0          | 0      | 1       | | ||||
|     And I entered the course "Course 1" as "student1" in the app | ||||
|     And I press "Basic book" in the app | ||||
|     Then I should find "1. Chapt 1" in the app | ||||
|     And I should find "1.1. Chapt 1.1" in the app | ||||
|     And I should find "2. Chapt 2" in the app | ||||
|     And I should find "3. Chapt 3" in the app | ||||
| 
 | ||||
|     When I press "Start" in the app | ||||
|     And I press "Table of contents" in the app | ||||
|     Then I should find "1. Chapt 1" in the app | ||||
|     And I should find "1.1. Chapt 1.1" in the app | ||||
|     And I should find "2. Chapt 2" in the app | ||||
|     And I should find "3. Chapt 3" in the app | ||||
| 
 | ||||
|     When I press "Close" in the app | ||||
|     And I press the back button in the app | ||||
|     And I press the back button in the app | ||||
|     And I press "Bull book" in the app | ||||
|     Then I should find "• Chapt 1" in the app | ||||
|     But I should not find "1. Chapt 1" in the app | ||||
| 
 | ||||
|     When I press "Start" in the app | ||||
|     And I press "Table of contents" in the app | ||||
|     Then I should find "• Chapt 1" in the app | ||||
|     But I should not find "1. Chapt 1" in the app | ||||
| 
 | ||||
|     When I press "Close" in the app | ||||
|     And I press the back button in the app | ||||
|     And I press the back button in the app | ||||
|     And I press "Ind book" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     But I should not find "• Chapt 1" in the app | ||||
|     And I should not find "1. Chapt 1" in the app | ||||
| 
 | ||||
|     When I press "Start" in the app | ||||
|     And I press "Table of contents" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     But I should not find "• Chapt 1" in the app | ||||
|     And I should not find "1. Chapt 1" in the app | ||||
| 
 | ||||
|     When I press "Close" in the app | ||||
|     And I press the back button in the app | ||||
|     And I press the back button in the app | ||||
|     And I press "None book" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     But I should not find "• Chapt 1" in the app | ||||
|     And I should not find "1. Chapt 1" in the app | ||||
| 
 | ||||
|     When I press "Start" in the app | ||||
|     And I press "Table of contents" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     But I should not find "• Chapt 1" in the app | ||||
|     And I should not find "1. Chapt 1" in the app | ||||
| 
 | ||||
|   Scenario: Test numbering (teacher) | ||||
|     Given I entered the course "Course 1" as "teacher1" in the app | ||||
|     And I press "Basic book" in the app | ||||
|     Then I should find "1. Chapt 1" in the app | ||||
|     And I should find "1.1. Chapt 1.1" in the app | ||||
|     And I should find "2. Chapt 2" in the app | ||||
|     And I should find "x. Hidden chapter" in the app | ||||
|     And I should find "x.x. Hidden subchapter" in the app | ||||
|     And I should find "3. Chapt 3" in the app | ||||
|     And I should find "3.x. Last hidden" in the app | ||||
							
								
								
									
										33
									
								
								src/addons/mod/book/tests/behat/single_activity.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/addons/mod/book/tests/behat/single_activity.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| @app @javascript @mod @mod_book | ||||
| Feature: Test single activity of book type in app | ||||
|   In order to view a book while using the mobile app | ||||
|   As a student | ||||
|   I need single activity of book type functionality to work | ||||
| 
 | ||||
|   Background: | ||||
|     Given the following "users" exist: | ||||
|       | username | firstname | lastname | | ||||
|       | student1 | First     | Student  | | ||||
|     And the following "courses" exist: | ||||
|       | fullname | shortname | category | format         | activitytype | | ||||
|       | Course 1 | C1        | 0        | singleactivity | book         | | ||||
|     And the following "course enrolments" exist: | ||||
|       | user     | course | role    | | ||||
|       | student1 | C1     | student | | ||||
|     And the following "activity" exist: | ||||
|       | activity | name                 | intro                 | course | idnumber | numbering | section | | ||||
|       | book     | Single activity book | Test book description | C1     | 1        | 1         | 0       | | ||||
|     And the following "mod_book > chapter" exist: | ||||
|       | book                 | title   | content                    | subchapter | hidden | pagenum | | ||||
|       | Single activity book | Chapt 1 | This is the first chapter  | 0          | 0      | 1       | | ||||
|       | Single activity book | Chapt 2 | This is the second chapter | 0          | 0      | 1       | | ||||
|       | Single activity book | Chapt 3 | This is the third chapter  | 0          | 0      | 1       | | ||||
| 
 | ||||
|   Scenario: Single activity book | ||||
|     Given I entered the course "Course 1" as "student1" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "Chapt 2" in the app | ||||
|     And I press "Chapt 1" in the app | ||||
|     Then I should find "Chapt 1" in the app | ||||
|     And I should find "This is the first chapter" in the app | ||||
|     But I should not find "This is the second chapter" in the app | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'addon.mod_chat.currentusers' | translate }}</h2> | ||||
|             <h1>{{ 'addon.mod_chat.currentusers' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'addon.mod_data.search' | translate }}</h2> | ||||
|             <h1>{{ 'addon.mod_data.search' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
| // 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 { | ||||
|     AddonModDataEntryField, | ||||
|     AddonModDataField, | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
| // 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 } from '@angular/core'; | ||||
| import { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data'; | ||||
| import { AddonModDataFieldPluginBaseComponent } from '@addons/mod/data/classes/base-field-plugin-component'; | ||||
|  | ||||
| @ -18,8 +18,8 @@ import { Component } from '@angular/core'; | ||||
| import { FormBuilder } from '@angular/forms'; | ||||
| import { SafeUrl } from '@angular/platform-browser'; | ||||
| import { CoreAnyError } from '@classes/errors/error'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreGeolocation, CoreGeolocationError, CoreGeolocationErrorReason } from '@services/geolocation'; | ||||
| import { CorePlatform } from '@services/platform'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { DomSanitizer } from '@singletons'; | ||||
| 
 | ||||
| @ -73,7 +73,7 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginBa | ||||
|             const northFixed = north ? north.toFixed(4) : '0.0000'; | ||||
|             const eastFixed = east ? east.toFixed(4) : '0.0000'; | ||||
| 
 | ||||
|             if (CoreApp.isIOS()) { | ||||
|             if (CorePlatform.isIOS()) { | ||||
|                 url = 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed; | ||||
|             } else { | ||||
|                 url = 'geo:' + northFixed + ',' + eastFixed; | ||||
|  | ||||
| @ -45,17 +45,17 @@ export class AddonModDataFieldNumberHandlerService extends AddonModDataFieldText | ||||
|         originalFieldData: AddonModDataEntryField, | ||||
|     ): boolean { | ||||
|         const fieldName = 'f_' + field.id; | ||||
|         const input = inputData[fieldName] || ''; | ||||
|         const content = originalFieldData?.content || ''; | ||||
|         const input = inputData[fieldName] ?? ''; | ||||
|         const content = originalFieldData?.content ?? ''; | ||||
| 
 | ||||
|         return input != content; | ||||
|         return input !== content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined { | ||||
|         if (field.required && (!inputData || !inputData.length || inputData[0].value == '')) { | ||||
|         if (field.required && (!inputData || !inputData.length || inputData[0].value === '')) { | ||||
|             return Translate.instant('addon.mod_data.errormustsupplyvalue'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
| // 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 { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data'; | ||||
| import { Component } from '@angular/core'; | ||||
| import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
| // 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 } from '@angular/core'; | ||||
| import { AddonModDataFieldPluginBaseComponent } from '../../../classes/base-field-plugin-component'; | ||||
| 
 | ||||
|  | ||||
| @ -70,7 +70,7 @@ export class AddonModDataFieldTextHandlerService implements AddonModDataFieldHan | ||||
| 
 | ||||
|         return [{ | ||||
|             fieldid: field.id, | ||||
|             value: inputData[fieldName] || '', | ||||
|             value: inputData[fieldName] ?? '', | ||||
|         }]; | ||||
|     } | ||||
| 
 | ||||
| @ -83,10 +83,10 @@ export class AddonModDataFieldTextHandlerService implements AddonModDataFieldHan | ||||
|         originalFieldData: AddonModDataEntryField, | ||||
|     ): boolean { | ||||
|         const fieldName = 'f_' + field.id; | ||||
|         const input = inputData[fieldName] || ''; | ||||
|         const content = originalFieldData?.content || ''; | ||||
|         const input = inputData[fieldName] ?? ''; | ||||
|         const content = originalFieldData?.content ?? ''; | ||||
| 
 | ||||
|         return input != content; | ||||
|         return input !== content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -102,7 +102,7 @@ export class AddonModDataFieldTextHandlerService implements AddonModDataFieldHan | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string>): AddonModDataEntryField { | ||||
|         originalContent.content = offlineContent[''] || ''; | ||||
|         originalContent.content = offlineContent[''] ?? ''; | ||||
| 
 | ||||
|         return originalContent; | ||||
|     } | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
| // 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 { AddonModDataEntryField } from '@addons/mod/data/services/data'; | ||||
| import { Component } from '@angular/core'; | ||||
| import { AddonModDataFieldPluginBaseComponent } from '../../../classes/base-field-plugin-component'; | ||||
|  | ||||
| @ -42,6 +42,7 @@ import { | ||||
| import { AddonModDataHelper } from '../../services/data-helper'; | ||||
| import { CoreDom } from '@singletons/dom'; | ||||
| import { AddonModDataEntryFieldInitialized } from '../../classes/base-field-plugin-component'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the view edit page. | ||||
| @ -368,9 +369,18 @@ export class AddonModDataEditPage implements OnInit { | ||||
|                             } | ||||
|                         }); | ||||
|                     } | ||||
| 
 | ||||
|                     this.jsData!.errors = this.errors; | ||||
| 
 | ||||
|                     this.scrollToFirstError(); | ||||
| 
 | ||||
|                     if (updateEntryResult.generalnotifications?.length) { | ||||
|                         CoreDomUtils.showAlertWithOptions({ | ||||
|                             header: Translate.instant('core.notice'), | ||||
|                             message: CoreTextUtils.buildMessage(updateEntryResult.generalnotifications), | ||||
|                             buttons: [Translate.instant('core.ok')], | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|             } finally { | ||||
|                 modal.dismiss(); | ||||
|  | ||||
| @ -590,8 +590,8 @@ export class AddonModDataHelperProvider { | ||||
|                 // WS wants values in JSON format.
 | ||||
|                 entryFieldDataToSend.push({ | ||||
|                     fieldid: fieldSubdata.fieldid, | ||||
|                     subfield: fieldSubdata.subfield || '', | ||||
|                     value: value ? JSON.stringify(value) : '', | ||||
|                     subfield: fieldSubdata.subfield ?? '', | ||||
|                     value: (value || value === 0) ? JSON.stringify(value) : '', | ||||
|                 }); | ||||
| 
 | ||||
|                 return; | ||||
|  | ||||
| @ -206,3 +206,22 @@ Feature: Users can manage entries in database activities | ||||
|     Then I should find "Are you sure you want to delete this entry?" in the app | ||||
|     And I press "Delete" in the app | ||||
|     And I should not find "Moodle Cloud" in the app | ||||
| 
 | ||||
|   Scenario: Handle number 0 correctly when creating entries | ||||
|     Given the following "activities" exist: | ||||
|       | activity | name      | intro     | course | idnumber | | ||||
|       | data     | Number DB | Number DB | C1     | data2    | | ||||
|     And the following "mod_data > fields" exist: | ||||
|       | database | type   | name   | description  | | ||||
|       | data2    | number | Number | Number value | | ||||
|     And I entered the data activity "Number DB" on course "Course 1" as "student1" in the app | ||||
|     When I press "Add entries" in the app | ||||
|     And I press "Save" near "Number DB" in the app | ||||
|     Then I should find "You did not fill out any fields!" in the app | ||||
| 
 | ||||
|     When I press "OK" in the app | ||||
|     And I set the following fields to these values in the app: | ||||
|       | Number | 0 | | ||||
|     And I press "Save" near "Number DB" in the app | ||||
|     Then I should find "0" near "Number:" in the app | ||||
|     But I should not find "Save" in the app | ||||
|  | ||||
| @ -37,7 +37,8 @@ | ||||
|     <div collapsible-footer *ngIf="!showLoading" slot="fixed"> | ||||
|         <div class="list-item-limited-width adaptable-buttons-row" | ||||
|             *ngIf="access && (access.canedititems || access.canviewreports || !access.isempty)"> | ||||
|             <ion-button expand="block" fill="outline" (click)="gotoAnswerQuestions(true)" class="ion-margin ion-text-wrap"> | ||||
|             <ion-button *ngIf="access.canedititems || access.canviewreports" expand="block" fill="outline" | ||||
|                 (click)="gotoAnswerQuestions(true)" class="ion-margin ion-text-wrap"> | ||||
|                 <ion-icon name="fas-search" slot="start" aria-hidden="true"></ion-icon> | ||||
|                 {{ 'addon.mod_feedback.preview' | translate }} | ||||
|             </ion-button> | ||||
| @ -64,7 +65,7 @@ | ||||
|         <ion-item class="ion-text-wrap" (click)="openAttempts()" [class.hide-detail]="!(access.canviewreports && completedCount > 0)" | ||||
|             [detail]="true" [button]="access.canviewreports && completedCount > 0"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_feedback.completed_feedbacks' | translate }}</h2> | ||||
|                 <p class="item-heading">{{ 'addon.mod_feedback.completed_feedbacks' | translate }}</p> | ||||
|             </ion-label> | ||||
|             <ion-badge slot="end"> | ||||
|                 <span aria-hidden="true">{{completedCount}}</span> | ||||
| @ -76,12 +77,12 @@ | ||||
|         <ion-item class="ion-text-wrap" *ngIf="!access.isanonymous && access.canviewreports" (click)="openNonRespondents()" detail="true" | ||||
|             button> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_feedback.show_nonrespondents' | translate }}</h2> | ||||
|                 <p class="item-heading">{{ 'addon.mod_feedback.show_nonrespondents' | translate }}</p> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <h2>{{ 'addon.mod_feedback.questions' | translate }}</h2> | ||||
|                 <p class="item-heading">{{ 'addon.mod_feedback.questions' | translate }}</p> | ||||
|             </ion-label> | ||||
|             <ion-badge slot="end"> | ||||
|                 <span aria-hidden="true">{{itemsCount}}</span> | ||||
| @ -115,19 +116,19 @@ | ||||
|         <ion-list *ngIf="access && (access.canedititems || access.canviewreports || !access.isempty)"> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="access.canedititems && overview.timeopen"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_feedback.feedbackopen' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_feedback.feedbackopen' | translate }}</p> | ||||
|                     <p>{{overview.openTimeReadable}}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="access.canedititems && overview.timeclose"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_feedback.feedbackclose' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_feedback.feedbackclose' | translate }}</p> | ||||
|                     <p>{{overview.closeTimeReadable}}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="access.canedititems && feedback && feedback.page_after_submit"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_feedback.page_after_submit' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_feedback.page_after_submit' | translate }}</p> | ||||
|                     <core-format-text [component]="component" [componentId]="componentId" [text]="feedback.page_after_submit" | ||||
|                         contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
| @ -136,7 +137,7 @@ | ||||
|             <ng-container *ngIf="!access.isempty"> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_feedback.mode' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_feedback.mode' | translate }}</p> | ||||
|                         <p *ngIf="access.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p> | ||||
|                         <p *ngIf="!access.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p> | ||||
|                     </ion-label> | ||||
|  | ||||
| @ -33,12 +33,12 @@ | ||||
|                     <core-spacer *ngIf="item.typ == 'pagebreak'"></core-spacer> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''"> | ||||
|                         <ion-label> | ||||
|                             <h2 *ngIf="item.name" [core-mark-required]="item.required"> | ||||
|                             <p class="item-heading" *ngIf="item.name" [core-mark-required]="item.required"> | ||||
|                                 <span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span> | ||||
|                                 <core-format-text [component]="component" [componentId]="cmId" [text]="item.name" contextLevel="module" | ||||
|                                     [contextInstanceId]="cmId" [courseId]="courseId"> | ||||
|                                 </core-format-text> | ||||
|                             </h2> | ||||
|                             </p> | ||||
|                             <p *ngIf="item.submittedValue"> | ||||
|                                 <core-format-text [component]="component" [componentId]="cmId" [text]="item.submittedValue" | ||||
|                                     contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId"> | ||||
|  | ||||
| @ -17,7 +17,7 @@ | ||||
|             <ion-list class="ion-no-margin has-spacer"> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_feedback.mode' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_feedback.mode' | translate }}</p> | ||||
|                         <p *ngIf="access!.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p> | ||||
|                         <p *ngIf="!access!.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p> | ||||
|                     </ion-label> | ||||
|  | ||||
| @ -109,6 +109,14 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { | ||||
| 
 | ||||
|         await this.fetchData(); | ||||
| 
 | ||||
|         if (!this.access || this.access.isempty && (!this.access.canedititems && !this.access.canviewreports)) { | ||||
|             CoreDomUtils.showErrorModal(Translate.instant('core.nopermissiontoaccesspage')); | ||||
| 
 | ||||
|             CoreNavigator.back(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.feedback) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -3,6 +3,8 @@ | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
|     <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||
|     <core-context-menu></core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| @ -74,26 +76,20 @@ | ||||
|                 [lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions?.getItemAriaCurrent(discussion)" | ||||
|                 (click)="discussions?.select(discussion)" button> | ||||
|                 <ion-label> | ||||
|                     <div class="addon-mod-forum-discussion-title"> | ||||
|                         <p class="ion-text-wrap item-heading"> | ||||
|                             <ion-icon name="fas-map-pin" *ngIf="discussion.pinned" | ||||
|                                 [attr.aria-label]="'addon.mod_forum.discussionpinned' | translate"></ion-icon> | ||||
|                             <ion-icon name="fas-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred" | ||||
|                                 [attr.aria-label]="'addon.mod_forum.favourites' | translate"></ion-icon> | ||||
|                             <core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id" | ||||
|                                 [courseId]="courseId"> | ||||
|                             </core-format-text> | ||||
|                             <ion-icon name="fas-lock" *ngIf="discussion.locked" class="addon-mod-forum-locked-icon" | ||||
|                                 [attr.aria-label]="'addon.mod_forum.discussionlocked' | translate"></ion-icon> | ||||
|                         </p> | ||||
|                         <ion-button *ngIf="canPin || discussion.canlock || discussion.canfavourite" fill="clear" | ||||
|                             [attr.aria-label]="('core.displayoptions' | translate)" (click)="showOptionsMenu($event, discussion)"> | ||||
|                             <ion-icon name="ellipsis-vertical" slot="icon-only" aria-hidden="true"> | ||||
|                             </ion-icon> | ||||
|                         </ion-button> | ||||
|                     </div> | ||||
|                     <p class="addon-mod-forum-discussion-title ion-text-wrap item-heading"> | ||||
|                         <ion-icon name="fas-map-pin" *ngIf="discussion.pinned" | ||||
|                             [attr.aria-label]="'addon.mod_forum.discussionpinned' | translate"></ion-icon> | ||||
|                         <ion-icon name="fas-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred" | ||||
|                             [attr.aria-label]="'addon.mod_forum.favourites' | translate"></ion-icon> | ||||
|                         <core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id" | ||||
|                             [courseId]="courseId"> | ||||
|                         </core-format-text> | ||||
|                         <ion-icon name="fas-lock" *ngIf="discussion.locked" class="addon-mod-forum-locked-icon" | ||||
|                             [attr.aria-label]="'addon.mod_forum.discussionlocked' | translate"></ion-icon> | ||||
|                     </p> | ||||
|                     <div class="addon-mod-forum-discussion-info"> | ||||
|                         <core-user-avatar *ngIf="discussion.userfullname" [user]="discussion" slot="start" [courseId]="courseId"> | ||||
|                         <core-user-avatar *ngIf="discussion.userfullname" [user]="discussion" slot="start" [courseId]="courseId" | ||||
|                             [linkProfile]="false"> | ||||
|                         </core-user-avatar> | ||||
|                         <div class="addon-mod-forum-discussion-author"> | ||||
|                             <span *ngIf="discussion.userfullname">{{discussion.userfullname}}</span> | ||||
| @ -136,6 +132,11 @@ | ||||
|                         </ion-col> | ||||
|                     </ion-row> | ||||
|                 </ion-label> | ||||
|                 <ion-button *ngIf="canPin || discussion.canlock || discussion.canfavourite" fill="clear" | ||||
|                     [attr.aria-label]="('core.displayoptions' | translate)" (click)="showOptionsMenu($event, discussion)" slot="end"> | ||||
|                     <ion-icon name="ellipsis-vertical" slot="icon-only" aria-hidden="true"> | ||||
|                     </ion-icon> | ||||
|                 </ion-button> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <core-infinite-loading [enabled]="discussions && discussions.loaded && !discussions.completed" [error]="fetchFailed" | ||||
|  | ||||
| @ -7,7 +7,6 @@ | ||||
|     } | ||||
| 
 | ||||
|     .addon-mod-forum-discussion.item { | ||||
| 
 | ||||
|         ion-label { | ||||
|             margin-top: 4px; | ||||
| 
 | ||||
| @ -35,21 +34,30 @@ | ||||
|             @include margin(0, 8px, 0, 0); | ||||
|         } | ||||
| 
 | ||||
|         .addon-mod-forum-discussion-title, | ||||
|         .addon-mod-forum-discussion-info { | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|         } | ||||
| 
 | ||||
|         .addon-mod-forum-discussion-title .item-heading, | ||||
|         .addon-mod-forum-discussion-info .addon-mod-forum-discussion-author { | ||||
|             flex-grow: 1; | ||||
|         } | ||||
| 
 | ||||
|         .addon-mod-forum-discussion-title { | ||||
|             @include margin-horizontal(null, 8px); | ||||
|             line-height: 18px; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         .addon-mod-forum-discussion-more-info.ios { | ||||
|             font-size: 0.9rem; | ||||
|         } | ||||
| 
 | ||||
|         ion-button { | ||||
|             position: absolute; | ||||
|             @include position (4px, 8px, null, null); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     .core-group-selector { | ||||
|  | ||||
| @ -126,7 +126,7 @@ | ||||
|                 <ion-icon *ngIf="advanced" name="fas-chevron-down" slot="start" aria-hidden="true" class="expandable-status-icon"> | ||||
|                 </ion-icon> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_forum.advanced' | translate }}</h2> | ||||
|                     <h3 class="item-heading">{{ 'addon.mod_forum.advanced' | translate }}</h3> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <div *ngIf="advanced" [id]="'addon-forum-reply-edit-form-advanced-' + uniqueId"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2 id="addon-mod-forum-sort-order-label">{{ 'core.sort' | translate }}</h2> | ||||
|             <h1 id="addon-mod-forum-sort-order-label">{{ 'core.sort' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
| @ -17,7 +17,7 @@ | ||||
|                 [attr.aria-current]="selected == sortOrder.value ? 'page' : 'false'" [attr.aria-label]="sortOrder.label | translate" | ||||
|                 (click)="selectSortOrder(sortOrder)" button aria-haspopup="dialog"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ sortOrder.label | translate }}</h2> | ||||
|                     <p class="item-heading">{{ sortOrder.label | translate }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ng-container> | ||||
|  | ||||
| @ -46,10 +46,10 @@ | ||||
|                     <ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups"></ion-toggle> | ||||
|                 </ion-item> | ||||
|                 <ion-item *ngIf="showGroups" class="core-edit-set-group"> | ||||
|                     <ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label> | ||||
|                     <ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" | ||||
|                         aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid" | ||||
|                         [interfaceOptions]="{header: 'addon.mod_forum.group' | translate}" (ionChange)="calculateGroupName()"> | ||||
|                     <ion-label>{{ 'addon.mod_forum.group' | translate }}</ion-label> | ||||
|                     <ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" interface="action-sheet" | ||||
|                         name="groupid" [interfaceOptions]="{header: 'addon.mod_forum.group' | translate}" | ||||
|                         (ionChange)="calculateGroupName()"> | ||||
|                         <ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option> | ||||
|                     </ion-select> | ||||
|                 </ion-item> | ||||
|  | ||||
| @ -28,11 +28,11 @@ | ||||
|                 </core-rich-text-editor> | ||||
|             </ion-item> | ||||
|             <ion-item *ngIf="categories.length > 0"> | ||||
|                 <ion-label position="stacked" id="addon-mod-glossary-categories-label"> | ||||
|                 <ion-label position="stacked"> | ||||
|                     {{ 'addon.mod_glossary.categories' | translate }} | ||||
|                 </ion-label> | ||||
|                 <ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label" | ||||
|                     interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories" | ||||
|                 <ion-select [(ngModel)]="options.categories" multiple="true" interface="action-sheet" | ||||
|                     [placeholder]="'addon.mod_glossary.categories' | translate" name="categories" | ||||
|                     [interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}"> | ||||
|                     <ion-select-option *ngFor="let category of categories" [value]="category.id"> | ||||
|                         {{ category.name }} | ||||
| @ -40,11 +40,10 @@ | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
|             <ion-item> | ||||
|                 <ion-label position="stacked" id="addon-mod-glossary-aliases-label"> | ||||
|                 <ion-label position="stacked"> | ||||
|                     {{ 'addon.mod_glossary.aliases' | translate }} | ||||
|                 </ion-label> | ||||
|                 <ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases" | ||||
|                     aria-labelledby="addon-mod-glossary-aliases-label" name="aliases"> | ||||
|                 <ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases" name="aliases"> | ||||
|                 </ion-textarea> | ||||
|             </ion-item> | ||||
|             <ion-item-divider> | ||||
|  | ||||
| @ -200,6 +200,7 @@ Feature: Test glossary navigation | ||||
|     When I swipe to the left in the app | ||||
|     Then I should find "Acerola is a fruit" in the app | ||||
| 
 | ||||
|   @ci_jenkins_skip | ||||
|   Scenario: Tablet navigation on glossary | ||||
|     Given I entered the course "Course 1" as "student1" in the app | ||||
|     And I change viewport size to "1200x640" | ||||
|  | ||||
| @ -57,7 +57,7 @@ | ||||
|         <ion-item class="ion-text-center" *ngIf="downloading"> | ||||
|             <ion-label> | ||||
|                 <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||
|                 <h2 *ngIf="progressMessage">{{ progressMessage | translate }}</h2> | ||||
|                 <p class="item-heading" *ngIf="progressMessage">{{ progressMessage | translate }}</p> | ||||
|                 <core-progress-bar *ngIf="showPercentage" [progress]="percentage" [a11yText]="progressMessage"></core-progress-bar> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|  | ||||
| @ -23,13 +23,13 @@ | ||||
|                 [attr.aria-label]="user.fullname"> | ||||
|                 <core-user-avatar [user]="user" slot="start" [courseId]="courseId" [linkProfile]="false"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}: {{user.fullname}}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}: {{user.fullname}}</p> | ||||
|                 </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> | ||||
|                     <p class="item-heading">{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
| 
 | ||||
| @ -38,13 +38,13 @@ | ||||
|                 <ion-list> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_h5pactivity.startdate' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_h5pactivity.startdate' | translate }}</p> | ||||
|                             <p>{{ attempt.timecreated | coreFormatDate:'strftimedatetime' }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_h5pactivity.completion' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_h5pactivity.completion' | translate }}</p> | ||||
|                             <p *ngIf="attempt.completion"> | ||||
|                                 <img src="assets/img/completion/completion-auto-y.svg" role="presentation" alt=""> | ||||
|                                 {{ 'addon.mod_h5pactivity.attempt_completion_yes' | translate }} | ||||
| @ -57,13 +57,13 @@ | ||||
|                     </ion-item> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_h5pactivity.duration' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_h5pactivity.duration' | translate }}</p> | ||||
|                             <p>{{ attempt.durationReadable }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_h5pactivity.outcome' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_h5pactivity.outcome' | translate }}</p> | ||||
|                             <p *ngIf="attempt.success !== null && attempt.success"> | ||||
|                                 <ion-icon name="fas-check-circle" aria-hidden="true"></ion-icon> | ||||
|                                 {{ 'addon.mod_h5pactivity.attempt_success_pass' | translate }} | ||||
| @ -79,7 +79,7 @@ | ||||
|                     </ion-item> | ||||
|                     <ion-item *ngIf="attempt.maxscore" class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_h5pactivity.totalscore' | translate }}</h2> | ||||
|                             <p class="item-heading">{{ 'addon.mod_h5pactivity.totalscore' | translate }}</p> | ||||
|                             <p>{{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: attempt} }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|  | ||||
| @ -37,7 +37,7 @@ | ||||
|             <ng-container *ngIf="attemptsData.scored"> | ||||
|                 <ion-item-divider> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ attemptsData.scored.title }}</h2> | ||||
|                         <h3 class="item-heading">{{ attemptsData.scored.title }}</h3> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
|                 <ng-container *ngTemplateOutlet="attemptsTemplate; context: {attempts: attemptsData.scored.attempts}"> | ||||
| @ -48,7 +48,7 @@ | ||||
|             <ng-container *ngIf="attemptsData.attempts && attemptsData.attempts.length"> | ||||
|                 <ion-item-divider> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_h5pactivity.all_attempts' | translate }}</h2> | ||||
|                         <h3 class="item-heading">{{ 'addon.mod_h5pactivity.all_attempts' | translate }}</h3> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
|                 <ng-container *ngTemplateOutlet="attemptsTemplate; context: {attempts: attemptsData.attempts}"></ng-container> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'addon.mod_imscp.toc' | translate }}</h2> | ||||
|             <h1>{{ 'addon.mod_imscp.toc' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -77,7 +77,7 @@ | ||||
|                         <ion-grid class="ion-text-wrap ion-hide-md-down"> | ||||
|                             <ion-row *ngIf="overview.lessonscored"> | ||||
|                                 <ion-col class="ion-text-center"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.averagescore' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.averagescore' | translate }}</p> | ||||
|                                     <p *ngIf="overview.numofattempts > 0"> | ||||
|                                         {{ 'core.percentagenumber' | translate:{$a: overview.avescore} }} | ||||
|                                     </p> | ||||
| @ -85,7 +85,7 @@ | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col class="ion-text-center"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</p> | ||||
|                                     <p *ngIf="overview.highscore != null"> | ||||
|                                         {{ 'core.percentagenumber' | translate:{$a: overview.highscore} }} | ||||
|                                     </p> | ||||
| @ -93,7 +93,7 @@ | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col class="ion-text-center"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</p> | ||||
|                                     <p *ngIf="overview.lowscore != null"> | ||||
|                                         {{ 'core.percentagenumber' | translate:{$a: overview.lowscore} }} | ||||
|                                     </p> | ||||
| @ -102,7 +102,7 @@ | ||||
|                             </ion-row> | ||||
|                             <ion-row> | ||||
|                                 <ion-col class="ion-text-center"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</p> | ||||
|                                     <p *ngIf="overview.avetime != null && overview.numofattempts">{{ avetimeReadable }}</p> | ||||
|                                     <p *ngIf="overview.avetime == null || !overview.numofattempts"> | ||||
|                                         {{ 'addon.mod_lesson.notcompleted' | translate }} | ||||
| @ -110,13 +110,13 @@ | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col class="ion-text-center"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</p> | ||||
|                                     <p *ngIf="overview.hightime != null">{{ hightimeReadable }}</p> | ||||
|                                     <p *ngIf="overview.hightime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col class="ion-text-center"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</p> | ||||
|                                     <p *ngIf="overview.lowtime != null">{{ lowtimeReadable }}</p> | ||||
|                                     <p *ngIf="overview.lowtime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                 </ion-col> | ||||
| @ -127,7 +127,7 @@ | ||||
|                         <ion-grid class="ion-text-wrap ion-hide-md-up"> | ||||
|                             <ion-row> | ||||
|                                 <ion-col class="ion-text-center" *ngIf="overview.lessonscored"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.averagescore' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.averagescore' | translate }}</p> | ||||
|                                     <p *ngIf="overview.numofattempts > 0"> | ||||
|                                         {{ 'core.percentagenumber' | translate:{$a: overview.avescore} }} | ||||
|                                     </p> | ||||
| @ -135,7 +135,7 @@ | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col [ngClass]="{'ion-text-center': overview.lessonscored}"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</p> | ||||
|                                     <p *ngIf="overview.avetime != null && overview.numofattempts">{{ avetimeReadable }}</p> | ||||
|                                     <p *ngIf="overview.avetime == null || !overview.numofattempts"> | ||||
|                                         {{ 'addon.mod_lesson.notcompleted' | translate }} | ||||
| @ -144,7 +144,7 @@ | ||||
|                             </ion-row> | ||||
|                             <ion-row> | ||||
|                                 <ion-col class="ion-text-center" *ngIf="overview.lessonscored"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</p> | ||||
|                                     <p *ngIf="overview.highscore != null"> | ||||
|                                         {{ 'core.percentagenumber' | translate:{$a: overview.highscore} }} | ||||
|                                     </p> | ||||
| @ -152,14 +152,14 @@ | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col [ngClass]="{'ion-text-center': overview.lessonscored}"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</p> | ||||
|                                     <p *ngIf="overview.hightime != null">{{ hightimeReadable }}</p> | ||||
|                                     <p *ngIf="overview.hightime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                 </ion-col> | ||||
|                             </ion-row> | ||||
|                             <ion-row> | ||||
|                                 <ion-col class="ion-text-center" *ngIf="overview.lessonscored"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</p> | ||||
|                                     <p *ngIf="overview.lowscore != null"> | ||||
|                                         {{ 'core.percentagenumber' | translate:{$a: overview.lowscore} }} | ||||
|                                     </p> | ||||
| @ -167,7 +167,7 @@ | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col [ngClass]="{'ion-text-center': overview.lessonscored}"> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</p> | ||||
|                                     <p *ngIf="overview.lowtime != null">{{ lowtimeReadable }}</p> | ||||
|                                     <p *ngIf="overview.lowtime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p> | ||||
|                                 </ion-col> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ pageInstance?.lesson?.name }}</h2> | ||||
|             <h1>{{ pageInstance?.lesson?.name }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'core.login.password' | translate }}</h2> | ||||
|             <h1>{{ 'core.login.password' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -94,7 +94,7 @@ | ||||
|                             </ion-item> | ||||
|                             <ion-item class="ion-text-wrap" *ngIf="!question.textarea && question.useranswer"> | ||||
|                                 <ion-label> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p> | ||||
|                                     <p> | ||||
|                                         <core-format-text [component]="component" [componentId]="lesson?.coursemodule" | ||||
|                                             [text]="question.useranswer" contextLevel="module" [contextInstanceId]="lesson?.coursemodule" | ||||
| @ -138,14 +138,12 @@ | ||||
|                             <ion-item class="ion-text-wrap" *ngFor="let row of question.rows"> | ||||
|                                 <ion-label> | ||||
|                                     <p> | ||||
|                                         <core-format-text id="addon-mod_lesson-matching-{{row.id}}" [component]="component" | ||||
|                                             [componentId]="lesson?.coursemodule" [text]="row.text" contextLevel="module" | ||||
|                                             [contextInstanceId]="lesson?.coursemodule" [courseId]="courseId"> | ||||
|                                         <core-format-text [component]="component" [componentId]="lesson?.coursemodule" [text]="row.text" | ||||
|                                             contextLevel="module" [contextInstanceId]="lesson?.coursemodule" [courseId]="courseId"> | ||||
|                                         </core-format-text> | ||||
|                                     </p> | ||||
|                                 </ion-label> | ||||
|                                 <ion-select [id]="row.id" [formControlName]="row.name" interface="action-sheet" | ||||
|                                     [attr.aria-labelledby]="'addon-mod_lesson-matching-' + row.id"> | ||||
|                                 <ion-select [id]="row.id" [formControlName]="row.name" interface="action-sheet"> | ||||
|                                     <ion-select-option *ngFor="let option of row.options" [value]="option.value"> | ||||
|                                         {{option.label}} | ||||
|                                     </ion-select-option> | ||||
|  | ||||
| @ -27,9 +27,8 @@ | ||||
| 
 | ||||
|             <!-- Retake selector if there is more than one retake. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="student.attempts && student.attempts.length > 1"> | ||||
|                 <ion-label id="addon-mod_lesson-retakeslabel">{{ 'addon.mod_lesson.attemptheader' | translate }}</ion-label> | ||||
|                 <ion-select [(ngModel)]="selectedRetake" (ionChange)="changeRetake(selectedRetake!)" | ||||
|                     aria-labelledby="addon-mod_lesson-retakeslabel" interface="action-sheet" | ||||
|                 <ion-label>{{ 'addon.mod_lesson.attemptheader' | translate }}</ion-label> | ||||
|                 <ion-select [(ngModel)]="selectedRetake" (ionChange)="changeRetake(selectedRetake!)" interface="action-sheet" | ||||
|                     [interfaceOptions]="{header: 'addon.mod_lesson.attemptheader' | translate}"> | ||||
|                     <ion-select-option *ngFor="let retake of student.attempts" [value]="retake.try"> | ||||
|                         {{retake.label}} | ||||
| @ -44,12 +43,12 @@ | ||||
|                         <ion-grid class="ion-no-padding"> | ||||
|                             <ion-row> | ||||
|                                 <ion-col> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.grade' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.grade' | translate }}</p> | ||||
|                                     <p>{{ 'core.percentagenumber' | translate:{$a: retake.userstats.grade} }}</p> | ||||
|                                 </ion-col> | ||||
| 
 | ||||
|                                 <ion-col> | ||||
|                                     <h3 class="item-heading">{{ 'addon.mod_lesson.rawgrade' | translate }}</h3> | ||||
|                                     <p class="item-heading">{{ 'addon.mod_lesson.rawgrade' | translate }}</p> | ||||
|                                     <p>{{ retake.userstats.gradeinfo.earned }} / {{ retake.userstats.gradeinfo.total }}</p> | ||||
|                                 </ion-col> | ||||
|                             </ion-row> | ||||
| @ -58,13 +57,13 @@ | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h3 class="item-heading">{{ 'addon.mod_lesson.timetaken' | translate }}</h3> | ||||
|                         <p class="item-heading">{{ 'addon.mod_lesson.timetaken' | translate }}</p> | ||||
|                         <p>{{ timeTakenReadable }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h3 class="item-heading">{{ 'addon.mod_lesson.completed' | translate }}</h3> | ||||
|                         <p class="item-heading">{{ 'addon.mod_lesson.completed' | translate }}</p> | ||||
|                         <p>{{ retake.userstats.completed * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| @ -85,7 +84,7 @@ | ||||
|                     </ion-card-header> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h3 class="item-heading">{{ 'addon.mod_lesson.question' | translate }}</h3> | ||||
|                             <p class="item-heading">{{ 'addon.mod_lesson.question' | translate }}</p> | ||||
|                             <p> | ||||
|                                 <core-format-text [component]="component" [componentId]="lesson?.coursemodule" collapsible-item | ||||
|                                     [text]="page.contents" contextLevel="module" [contextInstanceId]="lesson?.coursemodule" | ||||
| @ -96,7 +95,7 @@ | ||||
|                     </ion-item> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h3 class="item-heading">{{ 'addon.mod_lesson.answer' | translate }}</h3> | ||||
|                             <p class="item-heading">{{ 'addon.mod_lesson.answer' | translate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="!page.answerdata || !page.answerdata.answers || !page.answerdata.answers.length"> | ||||
| @ -227,7 +226,7 @@ | ||||
| 
 | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="page.answerdata.response"> | ||||
|                             <ion-label> | ||||
|                                 <h3 class="item-heading">{{ 'addon.mod_lesson.response' | translate }}</h3> | ||||
|                                 <p class="item-heading">{{ 'addon.mod_lesson.response' | translate }}</p> | ||||
|                                 <p> | ||||
|                                     <core-format-text [component]="component" [componentId]="lesson?.coursemodule" | ||||
|                                         [text]="page.answerdata.response" contextLevel="module" [contextInstanceId]="lesson?.coursemodule" | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <ion-item class="ion-text-wrap"> | ||||
|     <ion-label> | ||||
|         <h3 class="item-heading">{{ 'core.settings.synchronization' | translate }}</h3> | ||||
|         <p class="item-heading">{{ 'core.settings.synchronization' | translate }}</p> | ||||
|         <p>{{ 'addon.mod_quiz.confirmcontinueoffline' | translate:{$a: syncTimeReadable} }}</p> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <ion-item class="ion-text-wrap"> | ||||
|     <ion-label> | ||||
|         <h3 class="item-heading">{{ 'addon.mod_quiz.quizpassword' | translate }}</h3> | ||||
|         <p class="item-heading">{{ 'addon.mod_quiz.quizpassword' | translate }}</p> | ||||
|         <p>{{ 'addon.mod_quiz.requirepasswordmessage' | translate}}</p> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <ion-item class="ion-text-wrap"> | ||||
|     <ion-label> | ||||
|         <h3 class="item-heading">{{ 'addon.mod_quiz.confirmstartheader' | translate }}</h3> | ||||
|         <p class="item-heading">{{ 'addon.mod_quiz.confirmstartheader' | translate }}</p> | ||||
|         <p>{{ 'addon.mod_quiz.confirmstart' | translate:{$a: readableTimeLimit} }}</p> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'addon.mod_quiz.quiznavigation' | translate }}</h2> | ||||
|             <h1>{{ 'addon.mod_quiz.quiznavigation' | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ title | translate }}</h2> | ||||
|             <h1>{{ title | translate }}</h1> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|  | ||||
| @ -20,32 +20,32 @@ | ||||
|         <ion-list *ngIf="attempt"> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_quiz.attemptnumber' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_quiz.attemptnumber' | translate }}</p> | ||||
|                     <p *ngIf="attempt.preview">{{ 'addon.mod_quiz.preview' | translate }}</p> | ||||
|                     <p *ngIf="!attempt.preview">{{ attempt.attempt }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_quiz.attemptstate' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p> | ||||
|                     <p *ngFor="let sentence of attempt.readableState">{{ sentence }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="quiz!.showMarkColumn && attempt.readableMark !== ''"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz!.sumGradesFormatted }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz!.sumGradesFormatted }}</p> | ||||
|                     <p>{{ attempt.readableMark }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="quiz!.showGradeColumn && attempt.readableGrade !== ''"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz!.gradeFormatted }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz!.gradeFormatted }}</p> | ||||
|                     <p>{{ attempt.readableGrade }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="quiz!.showFeedbackColumn && feedback"> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'addon.mod_quiz.feedback' | translate }}</h2> | ||||
|                     <p class="item-heading">{{ 'addon.mod_quiz.feedback' | translate }}</p> | ||||
|                     <p> | ||||
|                         <core-format-text [component]="component" [componentId]="componentId" [text]="feedback" contextLevel="module" | ||||
|                             [contextInstanceId]="cmId" [courseId]="courseId"> | ||||
|  | ||||
| @ -132,7 +132,7 @@ | ||||
|             <!-- List of messages explaining why the quiz cannot be submitted. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="preventSubmitMessages.length"> | ||||
|                 <ion-label> | ||||
|                     <h3 class="item-heading">{{ 'addon.mod_quiz.cannotsubmitquizdueto' | translate }}</h3> | ||||
|                     <p class="item-heading">{{ 'addon.mod_quiz.cannotsubmitquizdueto' | translate }}</p> | ||||
|                     <p *ngFor="let message of preventSubmitMessages">{{message}}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|  | ||||
| @ -47,7 +47,7 @@ import { CanLeave } from '@guards/can-leave'; | ||||
| import { CoreForms } from '@singletons/form'; | ||||
| import { CoreDom } from '@singletons/dom'; | ||||
| import { CoreTime } from '@singletons/time'; | ||||
| import { CoreComponentsRegistry } from '@singletons/components-registry'; | ||||
| import { CoreDirectivesRegistry } from '@singletons/directives-registry'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that allows attempting a quiz. | ||||
| @ -690,7 +690,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { | ||||
|      */ | ||||
|     protected async scrollToQuestion(slot: number): Promise<void> { | ||||
|         await CoreUtils.nextTick(); | ||||
|         await CoreComponentsRegistry.waitComponentsReady(this.elementRef.nativeElement, 'core-question'); | ||||
|         await CoreDirectivesRegistry.waitDirectivesReady(this.elementRef.nativeElement, 'core-question'); | ||||
|         await CoreDom.scrollToElement( | ||||
|             this.elementRef.nativeElement, | ||||
|             '#addon-mod_quiz-question-' + slot, | ||||
|  | ||||
| @ -26,43 +26,43 @@ | ||||
|             <ion-list> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_quiz.startedon' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_quiz.startedon' | translate }}</p> | ||||
|                         <p>{{ attempt.timestart! * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_quiz.attemptstate' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p> | ||||
|                         <p>{{ readableState }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="showCompleted"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_quiz.completedon' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_quiz.completedon' | translate }}</p> | ||||
|                         <p>{{ attempt.timefinish! * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="timeTaken"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_quiz.timetaken' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_quiz.timetaken' | translate }}</p> | ||||
|                         <p>{{ timeTaken }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="overTime"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_quiz.overdue' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_quiz.overdue' | translate }}</p> | ||||
|                         <p>{{ overTime }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="readableMark"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_quiz.marks' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }}</p> | ||||
|                         <p>{{ readableMark }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="readableGrade"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_quiz.grade' | translate }}</h2> | ||||
|                         <p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }}</p> | ||||
|                         <p>{{ readableGrade }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user