diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index d8ff19c9d..c23c2c183 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -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 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 746a6c38a..47e3fbf71 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 82f350279..1ff23c922 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index ae0e4be9f..1a9f4aa4c 100644 --- a/README.md +++ b/README.md @@ -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 ------- diff --git a/angular.json b/angular.json index 376a770fd..ec4ff0695 100644 --- a/angular.json +++ b/angular.json @@ -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": { diff --git a/config.xml b/config.xml index a97ad98de..cb9f500f4 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + Moodle Moodle official app Moodle Mobile team @@ -27,7 +27,7 @@ - + @@ -196,13 +196,9 @@ - - - - - + @@ -236,7 +232,7 @@ - 4.1.0 + 4.1.1 diff --git a/gulpfile.js b/gulpfile.js index 60451192f..d7098d9b7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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')); }); diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index 168bfa9d5..04395fbaf 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -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(); diff --git a/local_moodleappbehat/tests/behat/snapshots/failures/.gitkeep b/local_moodleappbehat/tests/behat/snapshots/failures/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/moodle.config.json b/moodle.config.json index 103c210ea..061b6a3ed 100644 --- a/moodle.config.json +++ b/moodle.config.json @@ -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, diff --git a/package-lock.json b/package-lock.json index 9a020e161..71817b7fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index e359872da..815c1605b 100644 --- a/package.json +++ b/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": {}, diff --git a/patches/event-target-shim+6.0.2.patch b/patches/event-target-shim+6.0.2.patch new file mode 100644 index 000000000..8d8947270 --- /dev/null +++ b/patches/event-target-shim+6.0.2.patch @@ -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, TMode extends "standard" | "strict"> = EventTarget & defineEventAttribute.EventAttributes; ++ type CustomEventTarget, TMode extends "standard" | "strict"> = EventTarget & defineEventAttribute.EventAttributes; + } + /** + * 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(target: TEventTarget, type: TEventType, _eventClass?: TEventConstrucor): asserts target is TEventTarget & defineEventAttribute.EventAttributes>>; ++export declare function defineEventAttribute(target: TEventTarget, type: TEventType, _eventClass?: TEventConstrucor): asserts target is TEventTarget & defineEventAttribute.EventAttributes; + export declare namespace defineEventAttribute { + /** + * Definition of event attributes. + */ +- type EventAttributes, TEventMap extends Record> = { +- [P in string & keyof TEventMap as `on${P}`]: EventTarget.CallbackFunction | null; +- }; ++ type EventAttributes> = Record | null>; + } + /** + * Set the warning handler. diff --git a/patches/mp3-mediarecorder+4.0.5.patch b/patches/mp3-mediarecorder+4.0.5.patch new file mode 100644 index 000000000..230e1e04f --- /dev/null +++ b/patches/mp3-mediarecorder+4.0.5.patch @@ -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 +- }); ++ })); + } + + } //------------------------------------------------------------------------------ diff --git a/resources/splash.png b/resources/splash.png index e7889ccf9..d44505493 100644 Binary files a/resources/splash.png and b/resources/splash.png differ diff --git a/scripts/build-behat-plugin.js b/scripts/build-behat-plugin.js index 9dc94bcc8..621ad007f 100755 --- a/scripts/build-behat-plugin.js +++ b/scripts/build-behat-plugin.js @@ -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) { diff --git a/scripts/copy-assets.js b/scripts/copy-assets.js index 3b3f03ea4..72274160b 100644 --- a/scripts/copy-assets.js +++ b/scripts/copy-assets.js @@ -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) { diff --git a/scripts/langindex.json b/scripts/langindex.json index 7d2407912..95e7f3887 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/src/addons/badges/pages/issued-badge/issued-badge.html b/src/addons/badges/pages/issued-badge/issued-badge.html index 5f233087f..abb339c92 100644 --- a/src/addons/badges/pages/issued-badge/issued-badge.html +++ b/src/addons/badges/pages/issued-badge/issued-badge.html @@ -33,7 +33,7 @@ -

{{ 'core.name' | translate}}

+

{{ 'core.name' | translate}}

{{ user.fullname }}

@@ -48,13 +48,13 @@ -

{{ 'addon.badges.issuername' | translate}}

+

{{ 'addon.badges.issuername' | translate}}

{{ badge.issuername }}

-

{{ 'addon.badges.contact' | translate}}

+

{{ 'addon.badges.contact' | translate}}

{{ badge.issuercontact }}

@@ -70,37 +70,37 @@ -

{{ 'core.name' | translate}}

+

{{ 'core.name' | translate}}

{{ badge.name }}

-

{{ 'addon.badges.version' | translate}}

+

{{ 'addon.badges.version' | translate}}

{{ badge.version }}

-

{{ 'addon.badges.language' | translate}}

+

{{ 'addon.badges.language' | translate}}

{{ badge.language }}

-

{{ 'core.description' | translate}}

+

{{ 'core.description' | translate}}

{{ badge.description }}

-

{{ 'addon.badges.imageauthorname' | translate}}

+

{{ 'addon.badges.imageauthorname' | translate}}

{{ badge.imageauthorname }}

-

{{ 'addon.badges.imageauthoremail' | translate}}

+

{{ 'addon.badges.imageauthoremail' | translate}}

{{ badge.imageauthoremail }}

@@ -108,19 +108,19 @@
-

{{ 'addon.badges.imageauthorurl' | translate}}

+

{{ 'addon.badges.imageauthorurl' | translate}}

{{ badge.imageauthorurl }}

-

{{ 'addon.badges.imagecaption' | translate}}

+

{{ 'addon.badges.imagecaption' | translate}}

{{ badge.imagecaption }}

-

{{ 'core.course' | translate}}

+

{{ 'core.course' | translate}}

@@ -138,13 +138,13 @@ -

{{ 'addon.badges.dateawarded' | translate}}

+

{{ 'addon.badges.dateawarded' | translate}}

{{badge.dateissued * 1000 | coreFormatDate }}

-

{{ 'addon.badges.expirydate' | translate}}

+

{{ 'addon.badges.expirydate' | translate}}

{{ badge.dateexpire * 1000 | coreFormatDate }} @@ -165,13 +165,13 @@ -

{{ 'addon.badges.issuername' | translate}}

+

{{ 'addon.badges.issuername' | translate}}

{{ badge.endorsement.issuername }}

-

{{ 'addon.badges.issueremail' | translate}}

+

{{ 'addon.badges.issueremail' | translate}}

{{ badge.endorsement.issueremail }} @@ -181,25 +181,25 @@ -

{{ 'addon.badges.issuerurl' | translate}}

+

{{ 'addon.badges.issuerurl' | translate}}

{{ badge.endorsement.issuerurl }}

-

{{ 'addon.badges.dateawarded' | translate}}

+

{{ 'addon.badges.dateawarded' | translate}}

{{ badge.endorsement.dateissued * 1000 | coreFormatDate }}

-

{{ 'addon.badges.claimid' | translate}}

+

{{ 'addon.badges.claimid' | translate}}

{{ badge.endorsement.claimid }}

-

{{ 'addon.badges.claimcomment' | translate}}

+

{{ 'addon.badges.claimcomment' | translate}}

{{ badge.endorsement.claimcomment }}

@@ -214,12 +214,12 @@ -

{{ relatedBadge.name }}

+

{{ relatedBadge.name }}

-

{{ 'addon.badges.norelated' | translate}}

+

{{ 'addon.badges.norelated' | translate}}

@@ -234,12 +234,12 @@ -

{{ alignment.targetname }}

+

{{ alignment.targetname }}

-

{{ 'addon.badges.noalignment' | translate}}

+

{{ 'addon.badges.noalignment' | translate}}

diff --git a/src/addons/block/timeline/tests/behat/basic_usage-311.feature b/src/addons/block/timeline/tests/behat/basic_usage-311.feature index e7501b5f9..d22c111bf 100644 --- a/src/addons/block/timeline/tests/behat/basic_usage-311.feature +++ b/src/addons/block/timeline/tests/behat/basic_usage-311.feature @@ -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## | diff --git a/src/addons/block/timeline/tests/behat/basic_usage.feature b/src/addons/block/timeline/tests/behat/basic_usage.feature index f69fc3f3a..9042ad307 100644 --- a/src/addons/block/timeline/tests/behat/basic_usage.feature +++ b/src/addons/block/timeline/tests/behat/basic_usage.feature @@ -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## | diff --git a/src/addons/blog/pages/entries/entries.html b/src/addons/blog/pages/entries/entries.html index 06d703932..44d416a2a 100644 --- a/src/addons/blog/pages/entries/entries.html +++ b/src/addons/blog/pages/entries/entries.html @@ -27,19 +27,22 @@ -

- - - +

+

+ + +

+ {{ 'addon.blog.' + entry.publishTranslated! | translate}} -

-

- +

+
+ {{entry.user && entry.user.fullname}} + {{entry.created | coreDateDayOrTime}} - {{entry.user && entry.user!.fullname}} -

+
diff --git a/src/addons/calendar/components/calendar/addon-calendar-calendar.html b/src/addons/calendar/components/calendar/addon-calendar-calendar.html index b355dd1e5..3578f740c 100644 --- a/src/addons/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addons/calendar/components/calendar/addon-calendar-calendar.html @@ -33,7 +33,7 @@ - +
@@ -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)"> -

+ }' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" + (ariaButtonClick)="dayClicked(day.mday)" [tabindex]="activeView ? 0 : -1"> +

{{ day.periodName | translate }}

@@ -72,8 +72,8 @@
+ [class.addon-calendar-event-past]="event.ispast" (ariaButtonClick)="eventClicked(event, $event)" + [tabindex]="activeView ? 0 : -1"> diff --git a/src/addons/calendar/components/calendar/calendar.scss b/src/addons/calendar/components/calendar/calendar.scss index 7f6833c23..0df183e40 100644 --- a/src/addons/calendar/components/calendar/calendar.scss +++ b/src/addons/calendar/components/calendar/calendar.scss @@ -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; } diff --git a/src/addons/calendar/pages/day/day.html b/src/addons/calendar/pages/day/day.html index 1abcf2c16..8607da154 100644 --- a/src/addons/calendar/pages/day/day.html +++ b/src/addons/calendar/pages/day/day.html @@ -38,7 +38,7 @@ -

{{ periodName }}

+

{{ periodName }}

diff --git a/src/addons/calendar/pages/day/day.scss b/src/addons/calendar/pages/day/day.scss index 145eccfb8..111ba6953 100644 --- a/src/addons/calendar/pages/day/day.scss +++ b/src/addons/calendar/pages/day/day.scss @@ -6,7 +6,7 @@ .addon-calendar-period { flex-grow: 3; - h3 { + h2 { margin-top: 10px; font-size: 1.2rem; } diff --git a/src/addons/calendar/pages/event/event.html b/src/addons/calendar/pages/event/event.html index e41c029cc..40adfcf4c 100644 --- a/src/addons/calendar/pages/event/event.html +++ b/src/addons/calendar/pages/event/event.html @@ -60,7 +60,7 @@ -

{{ 'addon.calendar.when' | translate }}

+

{{ 'addon.calendar.when' | translate }}

@@ -70,13 +70,13 @@
-

{{ 'addon.calendar.eventtype' | translate }}

+

{{ 'addon.calendar.eventtype' | translate }}

{{ 'addon.calendar.type' + event.formattedType | translate }}

-

{{ 'core.course' | translate}}

+

{{ 'core.course' | translate}}

@@ -85,13 +85,13 @@ -

{{ 'core.group' | translate}}

+

{{ 'core.group' | translate}}

{{ groupName }}

-

{{ 'core.category' | translate}}

+

{{ 'core.category' | translate}}

@@ -100,7 +100,7 @@ -

{{ 'core.description' | translate}}

+

{{ 'core.description' | translate}}

@@ -109,7 +109,7 @@ -

{{ 'core.location' | translate}}

+

{{ 'core.location' | translate}}

-

{{ user.fullname }}

+

{{ user.fullname }}

@@ -115,7 +115,7 @@
-

{{ 'addon.competency.evidence' | translate }}

+

{{ 'addon.competency.evidence' | translate }}

{{ 'addon.competency.noevidence' | translate }}

diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.html b/src/addons/competency/pages/coursecompetencies/coursecompetencies.html index daea95879..3d36e2321 100644 --- a/src/addons/competency/pages/coursecompetencies/coursecompetencies.html +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.html @@ -53,7 +53,7 @@ -

{{ user.fullname }}

+

{{ user.fullname }}

diff --git a/src/addons/competency/pages/plan/plan.html b/src/addons/competency/pages/plan/plan.html index 055972527..ec42cb599 100644 --- a/src/addons/competency/pages/plan/plan.html +++ b/src/addons/competency/pages/plan/plan.html @@ -17,7 +17,7 @@ -

{{ user.fullname }}

+

{{ user.fullname }}

diff --git a/src/addons/coursecompletion/pages/report/report.html b/src/addons/coursecompletion/pages/report/report.html index e9c516500..22420d5f9 100644 --- a/src/addons/coursecompletion/pages/report/report.html +++ b/src/addons/coursecompletion/pages/report/report.html @@ -16,20 +16,20 @@ -

{{user!.fullname}}

+

{{user.fullname}}

-

{{ 'addon.coursecompletion.status' | translate }}

+

{{ 'addon.coursecompletion.status' | translate }}

{{ statusText! | translate }}

-

{{ 'addon.coursecompletion.required' | translate }}

+

{{ 'addon.coursecompletion.required' | translate }}

{{ 'addon.coursecompletion.criteriarequiredall' | translate }}

{{ 'addon.coursecompletion.criteriarequiredany' | translate }}

diff --git a/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts b/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts new file mode 100644 index 000000000..3301aecb6 --- /dev/null +++ b/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts @@ -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 = (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 ( this.el_).videoWidth ?? 0; + } + + /** + * Get the video height. + * + * @returns Video heigth. + */ + videoHeight(): number { + return ( 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 { + ( this.el_).poster = url; + } + + /** + * Is the media preloaded or not. + * + * @returns Whether it's preloaded. + */ + preload(): PreloadOption { + return 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; +}; diff --git a/src/addons/filter/mediaplugin/mediaplugin.module.ts b/src/addons/filter/mediaplugin/mediaplugin.module.ts index 1977bd08d..69451421c 100644 --- a/src/addons/filter/mediaplugin/mediaplugin.module.ts +++ b/src/addons/filter/mediaplugin/mediaplugin.module.ts @@ -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); + }, }, ], }) diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts index 509451927..5c5cea576 100644 --- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts +++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts @@ -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 { + filter(text: string): string | Promise { 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('video, audio')); - const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'; - const data = 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; - }[]; -}; diff --git a/src/addons/filter/mediaplugin/services/videojs.ts b/src/addons/filter/mediaplugin/services/videojs.ts new file mode 100644 index 000000000..2bfb87f8f --- /dev/null +++ b/src/addons/filter/mediaplugin/services/videojs.ts @@ -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; + + /** + * Create a VideoJS player. + * + * @param element Media element. + */ + async createPlayer(element: HTMLVideoElement | HTMLAudioElement): Promise { + // 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(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 { + 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(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 { + 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; +}; diff --git a/src/addons/filter/mediaplugin/utils/videojs.ts b/src/addons/filter/mediaplugin/utils/videojs.ts new file mode 100644 index 000000000..6d074d473 --- /dev/null +++ b/src/addons/filter/mediaplugin/utils/videojs.ts @@ -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); +} diff --git a/src/addons/messages/components/conversation-info/conversation-info.html b/src/addons/messages/components/conversation-info/conversation-info.html index c72e7b344..38f7c081d 100644 --- a/src/addons/messages/components/conversation-info/conversation-info.html +++ b/src/addons/messages/components/conversation-info/conversation-info.html @@ -1,7 +1,7 @@ -

{{ 'addon.messages.groupinfo' | translate }}

+

{{ 'addon.messages.groupinfo' | translate }}

@@ -19,18 +19,18 @@
-

- +

-

-

{{ 'addon.messages.numparticipants' | translate:{$a: conversation!.membercount} }}

+

{{ 'addon.messages.numparticipants' | translate:{$a: conversation.membercount} }}

diff --git a/src/addons/messages/pages/contacts-35/contacts.html b/src/addons/messages/pages/contacts-35/contacts.html index 285f07231..1e52521df 100644 --- a/src/addons/messages/pages/contacts-35/contacts.html +++ b/src/addons/messages/pages/contacts-35/contacts.html @@ -7,6 +7,8 @@

{{ 'addon.messages.contacts' | translate }}

+ +
diff --git a/src/addons/messages/pages/contacts-35/contacts.page.ts b/src/addons/messages/pages/contacts-35/contacts.page.ts index b552e49c0..a5dd98e8a 100644 --- a/src/addons/messages/pages/contacts-35/contacts.page.ts +++ b/src/addons/messages/pages/contacts-35/contacts.page.ts @@ -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, + }); } /** diff --git a/src/addons/messages/pages/contacts/contacts.html b/src/addons/messages/pages/contacts/contacts.html index 0715dea69..00e28e4d0 100644 --- a/src/addons/messages/pages/contacts/contacts.html +++ b/src/addons/messages/pages/contacts/contacts.html @@ -10,6 +10,8 @@ + + diff --git a/src/addons/messages/pages/contacts/contacts.page.ts b/src/addons/messages/pages/contacts/contacts.page.ts index 97535862e..187a39293 100644 --- a/src/addons/messages/pages/contacts/contacts.page.ts +++ b/src/addons/messages/pages/contacts/contacts.page.ts @@ -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, + }); } /** diff --git a/src/addons/messages/pages/discussions-35/discussions.html b/src/addons/messages/pages/discussions-35/discussions.html index 63fa5426d..83053d987 100644 --- a/src/addons/messages/pages/discussions-35/discussions.html +++ b/src/addons/messages/pages/discussions-35/discussions.html @@ -7,6 +7,8 @@

{{ 'addon.messages.messages' | translate }}

+ + @@ -29,7 +31,7 @@ [attr.aria-label]="'addon.messages.contacts' | translate" detail="true" button> -

{{ 'addon.messages.contacts' | translate }}

+

{{ 'addon.messages.contacts' | translate }}

diff --git a/src/addons/messages/pages/discussions-35/discussions.page.ts b/src/addons/messages/pages/discussions-35/discussions.page.ts index ef4df1701..0706b9b9d 100644 --- a/src/addons/messages/pages/discussions-35/discussions.page.ts +++ b/src/addons/messages/pages/discussions-35/discussions.page.ts @@ -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, + }); } /** diff --git a/src/addons/messages/pages/group-conversations/group-conversations.html b/src/addons/messages/pages/group-conversations/group-conversations.html index 71f57c29d..223cc96d7 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.html +++ b/src/addons/messages/pages/group-conversations/group-conversations.html @@ -13,6 +13,8 @@ + + diff --git a/src/addons/messages/pages/group-conversations/group-conversations.page.ts b/src/addons/messages/pages/group-conversations/group-conversations.page.ts index d54e0b793..d250f26ad 100644 --- a/src/addons/messages/pages/group-conversations/group-conversations.page.ts +++ b/src/addons/messages/pages/group-conversations/group-conversations.page.ts @@ -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, + }); } /** diff --git a/src/addons/messages/pages/search/search.html b/src/addons/messages/pages/search/search.html index 133b5c5ae..9830db473 100644 --- a/src/addons/messages/pages/search/search.html +++ b/src/addons/messages/pages/search/search.html @@ -7,6 +7,8 @@

{{ 'addon.messages.searchcombined' | translate }}

+ + diff --git a/src/addons/messages/pages/search/search.page.ts b/src/addons/messages/pages/search/search.page.ts index 160b34f7e..8d309a17a 100644 --- a/src/addons/messages/pages/search/search.page.ts +++ b/src/addons/messages/pages/search/search.page.ts @@ -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, + }); } } diff --git a/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.html b/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.html index f24b5d9b3..79f6fb070 100644 --- a/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.html +++ b/src/addons/mod/assign/components/edit-feedback-modal/edit-feedback-modal.html @@ -1,7 +1,7 @@ -

{{ plugin.name }}

+

{{ plugin.name }}

diff --git a/src/addons/mod/assign/components/index/addon-mod-assign-index.html b/src/addons/mod/assign/components/index/addon-mod-assign-index.html index 170d86ab3..117b0c76d 100644 --- a/src/addons/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addons/mod/assign/components/index/addon-mod-assign-index.html @@ -25,7 +25,7 @@ -

{{ 'core.course.hiddenfromstudents' | translate }}

+

{{ 'core.course.hiddenfromstudents' | translate }}

{{ 'core.no' | translate }}

{{ 'core.yes' | translate }}

@@ -33,13 +33,13 @@ -

{{ 'addon.mod_assign.timeremaining' | translate }}

+

{{ 'addon.mod_assign.timeremaining' | translate }}

{{ timeRemaining }}

-

{{ 'addon.mod_assign.latesubmissions' | translate }}

+

{{ 'addon.mod_assign.latesubmissions' | translate }}

{{ lateSubmissions }}

@@ -47,8 +47,8 @@ -

{{ 'addon.mod_assign.numberofteams' | translate }}

-

{{ 'addon.mod_assign.numberofparticipants' | translate }}

+

{{ 'addon.mod_assign.numberofteams' | translate }}

+

{{ 'addon.mod_assign.numberofparticipants' | translate }}

@@ -66,7 +66,7 @@ [class.hide-detail]="!summary.submissiondraftscount" [detail]="true" [button]="summary.submissiondraftscount" (click)="goToSubmissionList(submissionStatusDraft, !!summary.submissiondraftscount)"> -

{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}

+

{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}

@@ -82,7 +82,7 @@ [class.hide-detail]="!summary.submissionssubmittedcount" [detail]="true" [button]="summary.submissionssubmittedcount" (click)="goToSubmissionList(submissionStatusSubmitted, !!summary.submissionssubmittedcount)"> -

{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}

+

{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}

@@ -98,7 +98,7 @@ [class.hide-detail]="!needsGradingAvailable" [detail]="true" [button]="needsGradingAvailable" (click)="goToSubmissionList(needGrading, needsGradingAvailable)"> -

{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}

+

{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}

diff --git a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html index 2d8ec7e07..122f5c79b 100644 --- a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html @@ -15,7 +15,7 @@ [attr.aria-label]="user!.fullname"> -

{{ user!.fullname }}

+

{{ user!.fullname }}

@@ -23,7 +23,7 @@ -

{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}

+

{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}

@@ -31,7 +31,7 @@ -

{{ 'addon.mod_assign.submissionstatus' | translate }}

+

{{ 'addon.mod_assign.submissionstatus' | translate }}

@@ -44,7 +44,7 @@ -

{{ 'addon.mod_assign.attemptnumber' | translate }}

+

{{ 'addon.mod_assign.attemptnumber' | translate }}

{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} @@ -59,7 +59,7 @@ -

{{ 'addon.mod_assign.submissionslocked' | translate }}

+

{{ 'addon.mod_assign.submissionslocked' | translate }}

@@ -77,7 +77,7 @@ -

{{ 'addon.mod_assign.duedate' | translate }}

+

{{ 'addon.mod_assign.duedate' | translate }}

{{ assign!.duedate * 1000 | coreFormatDate }}

{{ 'addon.mod_assign.duedateno' | translate }}

@@ -85,14 +85,14 @@ -

{{ 'addon.mod_assign.cutoffdate' | translate }}

+

{{ 'addon.mod_assign.cutoffdate' | translate }}

{{ assign!.cutoffdate * 1000 | coreFormatDate }}

-

{{ 'addon.mod_assign.extensionduedate' | translate }}

+

{{ 'addon.mod_assign.extensionduedate' | translate }}

{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}

@@ -100,7 +100,7 @@ -

{{ 'addon.mod_assign.timeremaining' | translate }}

+

{{ 'addon.mod_assign.timeremaining' | translate }}

@@ -111,7 +111,7 @@ -

{{ 'addon.mod_assign.timelimit' | translate }}

+

{{ 'addon.mod_assign.timelimit' | translate }}

{{ assign.timelimit | coreDuration }}

@@ -120,7 +120,7 @@ -

{{ 'addon.mod_assign.editingstatus' | translate }}

+

{{ 'addon.mod_assign.editingstatus' | translate }}

{{ 'addon.mod_assign.submissioneditable' | translate }}

{{ 'addon.mod_assign.submissionnoteditable' | translate }}

@@ -130,7 +130,7 @@ -

{{ 'addon.mod_assign.timemodified' | translate }}

+

{{ 'addon.mod_assign.timemodified' | translate }}

{{ userSubmission!.timemodified * 1000 | coreFormatDate }}

@@ -151,7 +151,7 @@ [attr.aria-label]="user.fullname"> -

{{ user.fullname }}

+

{{ user.fullname }}

@@ -257,7 +257,7 @@ -

{{ 'addon.mod_assign.currentgrade' | translate }}

+

{{ 'addon.mod_assign.currentgrade' | translate }}

@@ -273,7 +273,7 @@ Use a text input because otherwise we cannot readthe value if it has an invalid character. --> -

{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}

+

{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}

@@ -284,7 +284,7 @@ -

{{ 'addon.mod_assign.grade' | translate }}

+

{{ 'addon.mod_assign.grade' | translate }}

@@ -297,7 +297,7 @@ -

{{ outcome.name }}

+

{{ outcome.name }}

@@ -311,7 +311,7 @@ -

{{ 'addon.mod_assign.currentgrade' | translate }}

+

{{ 'addon.mod_assign.currentgrade' | translate }}

{{ grade.gradebookGrade }}

@@ -332,7 +332,7 @@ -

{{ 'addon.mod_assign.markingworkflowstate' | translate }}

+

{{ 'addon.mod_assign.markingworkflowstate' | translate }}

{{ workflowStatusTranslationId | translate }}

@@ -340,7 +340,7 @@ -

{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}

+

{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}

{{ 'addon.mod_assign.applytoteam' | translate }}

@@ -350,7 +350,7 @@ -

{{ 'addon.mod_assign.attemptsettings' | translate }}

+

{{ 'addon.mod_assign.attemptsettings' | translate }}

{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} @@ -376,8 +376,8 @@ [attr.aria-label]="grader!.fullname" detail="true"> -

{{ 'addon.mod_assign.gradedby' | translate }}

-

{{ grader!.fullname }}

+

{{ 'addon.mod_assign.gradedby' | translate }}

+

{{ grader!.fullname }}

{{ feedback!.gradeddate * 1000 | coreFormatDate }}

@@ -385,7 +385,7 @@ -

{{ 'addon.mod_assign.gradedon' | translate }}

+

{{ 'addon.mod_assign.gradedon' | translate }}

{{ feedback!.gradeddate * 1000 | coreFormatDate }}

diff --git a/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html index 549976d19..43f6b2009 100644 --- a/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html +++ b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html @@ -20,7 +20,7 @@ -

{{ 'addon.mod_assign.wordlimit' | translate }}

+

{{ 'addon.mod_assign.wordlimit' | translate }}

{{ 'core.numwords' | translate: {'$a': words + ' / ' + wordLimit} }}

diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.html b/src/addons/mod/bigbluebuttonbn/components/index/index.html index 2fceda715..782717ab0 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.html +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.html @@ -19,13 +19,13 @@ -

{{ 'addon.mod_bigbluebuttonbn.mod_form_field_openingtime' | translate }}

+

{{ 'addon.mod_bigbluebuttonbn.mod_form_field_openingtime' | translate }}

{{ meetingInfo.openingtime * 1000 | coreFormatDate }}

-

{{ 'addon.mod_bigbluebuttonbn.mod_form_field_closingtime' | translate }}

+

{{ 'addon.mod_bigbluebuttonbn.mod_form_field_closingtime' | translate }}

{{ meetingInfo.closingtime * 1000 | coreFormatDate }}

@@ -45,31 +45,31 @@ -

{{ 'addon.mod_bigbluebuttonbn.view_message_session_started_at' | translate }}

+

{{ 'addon.mod_bigbluebuttonbn.view_message_session_started_at' | translate }}

{{ meetingInfo.startedat * 1000 | coreFormatDate: "strftimetime" }}

-

+

{{ 'addon.mod_bigbluebuttonbn.view_message_moderators' | translate }} -

-

+

+

{{ 'addon.mod_bigbluebuttonbn.view_message_moderator' | translate }} -

+

{{ meetingInfo.moderatorcount }}

-

+

{{ 'addon.mod_bigbluebuttonbn.view_message_viewers' | translate }} -

-

+

+

{{ 'addon.mod_bigbluebuttonbn.view_message_viewer' | translate }} -

+

{{ meetingInfo.participantcount }}

@@ -108,7 +108,7 @@
-

{{ data.label }}

+

{{ data.label }}

diff --git a/src/addons/mod/book/components/index/addon-mod-book-index.html b/src/addons/mod/book/components/index/addon-mod-book-index.html index 380582d58..b186dfb3c 100644 --- a/src/addons/mod/book/components/index/addon-mod-book-index.html +++ b/src/addons/mod/book/components/index/addon-mod-book-index.html @@ -24,8 +24,8 @@ (click)="openBook(chapter.id)">

- {{chapter.indexNumber}}  - •  + {{chapter.indexNumber}} +

diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index 838f154e8..93c64972f 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -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 { + 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 { + await CoreNavigator.navigateToSitePath( + `${AddonModBookModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/contents`, + { params: { chapterId } }, + ); this.hasStartedBook = true; } diff --git a/src/addons/mod/book/components/toc/toc.html b/src/addons/mod/book/components/toc/toc.html index 42165ea28..a8a12c978 100644 --- a/src/addons/mod/book/components/toc/toc.html +++ b/src/addons/mod/book/components/toc/toc.html @@ -1,7 +1,7 @@ -

{{ 'addon.mod_book.toc' | translate }}

+

{{ 'addon.mod_book.toc' | translate }}

@@ -17,8 +17,8 @@ [attr.aria-current]="selected == chapter.id ? 'page' : 'false'" button [class.item-dimmed]="chapter.hidden" detail="false">

- {{chapter.indexNumber}}  - •  + {{chapter.indexNumber}} +

diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index 8681ef369..2365c5291 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -291,7 +291,9 @@ export class AddonModBookProvider { }); } - chapterNumber++; + if (!parseInt(chapter.hidden, 10)) { + chapterNumber++; + } }); return chapters; diff --git a/src/addons/mod/book/tests/behat/basic_usage.feature b/src/addons/mod/book/tests/behat/basic_usage.feature new file mode 100755 index 000000000..65a09b54e --- /dev/null +++ b/src/addons/mod/book/tests/behat/basic_usage.feature @@ -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 diff --git a/src/addons/mod/book/tests/behat/single_activity.feature b/src/addons/mod/book/tests/behat/single_activity.feature new file mode 100644 index 000000000..f247c2034 --- /dev/null +++ b/src/addons/mod/book/tests/behat/single_activity.feature @@ -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 diff --git a/src/addons/mod/chat/components/users-modal/users-modal.html b/src/addons/mod/chat/components/users-modal/users-modal.html index b0b09c140..0f06b26d2 100644 --- a/src/addons/mod/chat/components/users-modal/users-modal.html +++ b/src/addons/mod/chat/components/users-modal/users-modal.html @@ -1,7 +1,7 @@ -

{{ 'addon.mod_chat.currentusers' | translate }}

+

{{ 'addon.mod_chat.currentusers' | translate }}

diff --git a/src/addons/mod/data/components/search/search.html b/src/addons/mod/data/components/search/search.html index e7dee1e2e..1c7d2d852 100644 --- a/src/addons/mod/data/components/search/search.html +++ b/src/addons/mod/data/components/search/search.html @@ -1,7 +1,7 @@ -

{{ 'addon.mod_data.search' | translate }}

+

{{ 'addon.mod_data.search' | translate }}

diff --git a/src/addons/mod/data/fields/checkbox/services/handler.ts b/src/addons/mod/data/fields/checkbox/services/handler.ts index 2bc249e5e..569fb55f5 100644 --- a/src/addons/mod/data/fields/checkbox/services/handler.ts +++ b/src/addons/mod/data/fields/checkbox/services/handler.ts @@ -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, diff --git a/src/addons/mod/data/fields/file/component/file.ts b/src/addons/mod/data/fields/file/component/file.ts index 11abea5bb..210596929 100644 --- a/src/addons/mod/data/fields/file/component/file.ts +++ b/src/addons/mod/data/fields/file/component/file.ts @@ -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'; diff --git a/src/addons/mod/data/fields/latlong/component/latlong.ts b/src/addons/mod/data/fields/latlong/component/latlong.ts index 38398beae..17c7a4f18 100644 --- a/src/addons/mod/data/fields/latlong/component/latlong.ts +++ b/src/addons/mod/data/fields/latlong/component/latlong.ts @@ -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; diff --git a/src/addons/mod/data/fields/number/services/handler.ts b/src/addons/mod/data/fields/number/services/handler.ts index 05ea5dd74..5846662c4 100644 --- a/src/addons/mod/data/fields/number/services/handler.ts +++ b/src/addons/mod/data/fields/number/services/handler.ts @@ -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'); } } diff --git a/src/addons/mod/data/fields/picture/component/picture.ts b/src/addons/mod/data/fields/picture/component/picture.ts index 6b99f0c10..adf032eb1 100644 --- a/src/addons/mod/data/fields/picture/component/picture.ts +++ b/src/addons/mod/data/fields/picture/component/picture.ts @@ -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'; diff --git a/src/addons/mod/data/fields/radiobutton/component/radiobutton.ts b/src/addons/mod/data/fields/radiobutton/component/radiobutton.ts index 5653e8830..cfc0064fb 100644 --- a/src/addons/mod/data/fields/radiobutton/component/radiobutton.ts +++ b/src/addons/mod/data/fields/radiobutton/component/radiobutton.ts @@ -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'; diff --git a/src/addons/mod/data/fields/text/services/handler.ts b/src/addons/mod/data/fields/text/services/handler.ts index 8083b72b8..40088d230 100644 --- a/src/addons/mod/data/fields/text/services/handler.ts +++ b/src/addons/mod/data/fields/text/services/handler.ts @@ -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): AddonModDataEntryField { - originalContent.content = offlineContent[''] || ''; + originalContent.content = offlineContent[''] ?? ''; return originalContent; } diff --git a/src/addons/mod/data/fields/url/component/url.ts b/src/addons/mod/data/fields/url/component/url.ts index ca90c661f..8784b0748 100644 --- a/src/addons/mod/data/fields/url/component/url.ts +++ b/src/addons/mod/data/fields/url/component/url.ts @@ -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'; diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts index e95ec4a5e..6509bc4a4 100644 --- a/src/addons/mod/data/pages/edit/edit.ts +++ b/src/addons/mod/data/pages/edit/edit.ts @@ -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(); diff --git a/src/addons/mod/data/services/data-helper.ts b/src/addons/mod/data/services/data-helper.ts index c83abf47a..52b822c95 100644 --- a/src/addons/mod/data/services/data-helper.ts +++ b/src/addons/mod/data/services/data-helper.ts @@ -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; diff --git a/src/addons/mod/data/tests/behat/entries.feature b/src/addons/mod/data/tests/behat/entries.feature index 59320cca3..bd6707cdf 100644 --- a/src/addons/mod/data/tests/behat/entries.feature +++ b/src/addons/mod/data/tests/behat/entries.feature @@ -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 diff --git a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html index d416650ca..64187214e 100644 --- a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html @@ -37,7 +37,8 @@
- + {{ 'addon.mod_feedback.preview' | translate }} @@ -64,7 +65,7 @@ -

{{ 'addon.mod_feedback.completed_feedbacks' | translate }}

+

{{ 'addon.mod_feedback.completed_feedbacks' | translate }}

@@ -76,12 +77,12 @@ -

{{ 'addon.mod_feedback.show_nonrespondents' | translate }}

+

{{ 'addon.mod_feedback.show_nonrespondents' | translate }}

-

{{ 'addon.mod_feedback.questions' | translate }}

+

{{ 'addon.mod_feedback.questions' | translate }}

@@ -115,19 +116,19 @@ -

{{ 'addon.mod_feedback.feedbackopen' | translate }}

+

{{ 'addon.mod_feedback.feedbackopen' | translate }}

{{overview.openTimeReadable}}

-

{{ 'addon.mod_feedback.feedbackclose' | translate }}

+

{{ 'addon.mod_feedback.feedbackclose' | translate }}

{{overview.closeTimeReadable}}

-

{{ 'addon.mod_feedback.page_after_submit' | translate }}

+

{{ 'addon.mod_feedback.page_after_submit' | translate }}

@@ -136,7 +137,7 @@ -

{{ 'addon.mod_feedback.mode' | translate }}

+

{{ 'addon.mod_feedback.mode' | translate }}

{{ 'addon.mod_feedback.anonymous' | translate }}

{{ 'addon.mod_feedback.non_anonymous' | translate }}

diff --git a/src/addons/mod/feedback/pages/attempt/attempt.html b/src/addons/mod/feedback/pages/attempt/attempt.html index 41005c393..0a110f49b 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.html +++ b/src/addons/mod/feedback/pages/attempt/attempt.html @@ -33,12 +33,12 @@ -

+

{{item.itemnumber}}. -

+

diff --git a/src/addons/mod/feedback/pages/form/form.html b/src/addons/mod/feedback/pages/form/form.html index e16c65d39..9f07a0255 100644 --- a/src/addons/mod/feedback/pages/form/form.html +++ b/src/addons/mod/feedback/pages/form/form.html @@ -17,7 +17,7 @@ -

{{ 'addon.mod_feedback.mode' | translate }}

+

{{ 'addon.mod_feedback.mode' | translate }}

{{ 'addon.mod_feedback.anonymous' | translate }}

{{ 'addon.mod_feedback.non_anonymous' | translate }}

diff --git a/src/addons/mod/feedback/pages/form/form.ts b/src/addons/mod/feedback/pages/form/form.ts index 3ff6d0e44..ad96737b0 100644 --- a/src/addons/mod/feedback/pages/form/form.ts +++ b/src/addons/mod/feedback/pages/form/form.ts @@ -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; } diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index e4d953a31..90e871645 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -3,6 +3,8 @@ + + @@ -74,26 +76,20 @@ [lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions?.getItemAriaCurrent(discussion)" (click)="discussions?.select(discussion)" button> -
-

- - - - - -

- - - -
+

+ + + + + +

- +
{{discussion.userfullname}} @@ -136,6 +132,11 @@ + + + open this report on your browser.", + "reportsource": "Report source", + "timecreated": "Time created", + "showcolumns": "Show columns", + "hidecolumns": "Hide columns" +} diff --git a/src/core/features/reportbuilder/pages/list/list.html b/src/core/features/reportbuilder/pages/list/list.html new file mode 100644 index 000000000..066eb5352 --- /dev/null +++ b/src/core/features/reportbuilder/pages/list/list.html @@ -0,0 +1,37 @@ + + + + + + +

{{ 'core.reportbuilder.reports' | translate }}

+
+
+
+ + + + + + + + +

+ +

+

{{ report.sourcename }}

+
+
+
+ + + + + + + + +
+
diff --git a/src/core/features/reportbuilder/pages/list/list.ts b/src/core/features/reportbuilder/pages/list/list.ts new file mode 100644 index 000000000..2538cb260 --- /dev/null +++ b/src/core/features/reportbuilder/pages/list/list.ts @@ -0,0 +1,128 @@ +// (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 { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; +import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; +import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreReportBuilderReportsSource } from '@features/reportbuilder/classes/reports-source'; +import { CoreReportBuilder, CoreReportBuilderReport, REPORTS_LIST_LIMIT } from '@features/reportbuilder/services/reportbuilder'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { BehaviorSubject } from 'rxjs'; + +@Component({ + selector: 'core-report-builder-list', + templateUrl: './list.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { + + reports!: CoreListItemsManager; + + state$: Readonly> = new BehaviorSubject({ + page: 1, + perpage: REPORTS_LIST_LIMIT, + loaded: false, + loadMoreError: false, + }); + + constructor() { + try { + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreReportBuilderReportsSource, []); + this.reports = new CoreListItemsManager(source, CoreReportBuilderListPage); + } catch (error) { + CoreDomUtils.showErrorModal(error); + CoreNavigator.back(); + } + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + try { + await this.fetchReports(true); + this.updateState({ loaded: true }); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error loading reports'); + + this.reports.reset(); + } + } + + /** + * Update reports list or loads it. + * + * @param reload is reoading or not. + */ + async fetchReports(reload: boolean): Promise { + reload ? await this.reports.reload() : await this.reports.load(); + this.updateState({ loadMoreError: false }); + } + + /** + * Properties of the state to update. + * + * @param state Object to update. + */ + updateState(state: Partial): void { + const previousState = this.state$.getValue(); + this.state$.next({ ...previousState, ...state }); + } + + /** + * Load a new batch of Reports. + * + * @param complete Completion callback. + */ + async fetchMoreReports(complete: () => void): Promise { + try { + await this.fetchReports(false); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error loading more reports'); + + this.updateState({ loadMoreError: true }); + } + + complete(); + } + + /** + * Refresh reports list. + * + * @param ionRefresher ionRefresher. + */ + async refreshReports(ionRefresher?: IonRefresher): Promise { + await CoreUtils.ignoreErrors(CoreReportBuilder.invalidateReportsList()); + await CoreUtils.ignoreErrors(this.fetchReports(true)); + await ionRefresher?.complete(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.reports.destroy(); + } + +} + +type CoreReportBuilderListState = { + page: number; + perpage: number; + loaded: boolean; + loadMoreError: boolean; +}; diff --git a/src/core/features/reportbuilder/pages/report/report.html b/src/core/features/reportbuilder/pages/report/report.html new file mode 100644 index 000000000..f7151d711 --- /dev/null +++ b/src/core/features/reportbuilder/pages/report/report.html @@ -0,0 +1,23 @@ + + + + + + + + + + + +

+ +

+

{{ reportDetail.sourcename }}

+
+
+
+ + + + + diff --git a/src/core/features/reportbuilder/pages/report/report.ts b/src/core/features/reportbuilder/pages/report/report.ts new file mode 100644 index 000000000..958509675 --- /dev/null +++ b/src/core/features/reportbuilder/pages/report/report.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CoreReportBuilderReportSummaryComponent } from '@features/reportbuilder/components/report-summary/report-summary'; +import { CoreReportBuilderReportDetail } from '@features/reportbuilder/services/reportbuilder'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; + +@Component({ + selector: 'core-report-builder-report', + templateUrl: './report.html', +}) +export class CoreReportBuilderReportPage implements OnInit { + + reportId!: string; + reportDetail?: CoreReportBuilderReportDetail; + /** + * @inheritdoc + */ + ngOnInit(): void { + this.reportId = CoreNavigator.getRequiredRouteParam('id'); + } + + /** + * Save the report detail + * + * @param reportDetail it contents the detail of the report. + */ + loadReportDetail(reportDetail: CoreReportBuilderReportDetail): void { + this.reportDetail = reportDetail; + } + + openInfo(): void { + CoreDomUtils.openSideModal({ + component: CoreReportBuilderReportSummaryComponent, + componentProps: { reportDetail: this.reportDetail }, + }); + } + +} diff --git a/src/core/features/reportbuilder/reportbuilder-lazy.module.ts b/src/core/features/reportbuilder/reportbuilder-lazy.module.ts new file mode 100644 index 000000000..c5064d388 --- /dev/null +++ b/src/core/features/reportbuilder/reportbuilder-lazy.module.ts @@ -0,0 +1,44 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CoreReportBuilderComponentsModule } from './components/components.module'; +import { CoreReportBuilderListPage } from './pages/list/list'; +import { CoreReportBuilderReportPage } from './pages/report/report'; + +const routes: Routes = [ + { + path: '', + component: CoreReportBuilderListPage, + }, + { + path: ':id', + component: CoreReportBuilderReportPage, + }, +]; + +@NgModule({ + imports: [ + CoreSharedModule, + CoreReportBuilderComponentsModule, + RouterModule.forChild(routes), + ], + declarations: [ + CoreReportBuilderListPage, + CoreReportBuilderReportPage, + ], +}) +export class CoreReportBuilderLazyModule {} diff --git a/src/core/features/reportbuilder/reportbuilder.module.ts b/src/core/features/reportbuilder/reportbuilder.module.ts new file mode 100644 index 000000000..a905a6ced --- /dev/null +++ b/src/core/features/reportbuilder/reportbuilder.module.ts @@ -0,0 +1,44 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { CoreReportBuilderHandler, CoreReportBuilderHandlerService } from './services/handlers/reportbuilder'; + +const routes: Routes = [ + { + path: CoreReportBuilderHandlerService.PAGE_NAME, + loadChildren: () => import('./reportbuilder-lazy.module').then(m => m.CoreReportBuilderLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + ], + exports: [CoreMainMenuRoutingModule], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreUserDelegate.registerHandler(CoreReportBuilderHandler.instance); + }, + }, + ], +}) +export class CoreReportBuilderModule {} diff --git a/src/core/features/reportbuilder/services/handlers/reportbuilder.ts b/src/core/features/reportbuilder/services/handlers/reportbuilder.ts new file mode 100644 index 000000000..9aacb15ab --- /dev/null +++ b/src/core/features/reportbuilder/services/handlers/reportbuilder.ts @@ -0,0 +1,59 @@ +// (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 { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; +import { CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { CoreReportBuilder } from '../reportbuilder'; + +/** + * Handler to visualize custom reports. + */ +@Injectable({ providedIn: 'root' }) +export class CoreReportBuilderHandlerService implements CoreUserProfileHandler { + + static readonly PAGE_NAME = 'reportbuilder'; + + type = CoreUserDelegateService.TYPE_NEW_PAGE; + cacheEnabled = true; + name = 'CoreReportBuilderDelegate'; + priority = 350; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return await CoreReportBuilder.isEnabled(); + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreUserProfileHandlerData { + return { + class: 'core-report-builder', + icon: 'fa-list-alt', + title: 'core.reportbuilder.reports', + action: async (event): Promise => { + event.preventDefault(); + event.stopPropagation(); + await CoreNavigator.navigateToSitePath(CoreReportBuilderHandlerService.PAGE_NAME); + }, + }; + } + +} + +export const CoreReportBuilderHandler = makeSingleton(CoreReportBuilderHandlerService); diff --git a/src/core/features/reportbuilder/services/reportbuilder.ts b/src/core/features/reportbuilder/services/reportbuilder.ts new file mode 100644 index 000000000..00dfac43f --- /dev/null +++ b/src/core/features/reportbuilder/services/reportbuilder.ts @@ -0,0 +1,267 @@ +// (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 { CoreError } from '@classes/errors/error'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreSites } from '@services/sites'; +import { CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'mmaReportBuilder:'; +export const REPORTS_LIST_LIMIT = 20; +export const REPORT_ROWS_LIMIT = 20; + +@Injectable({ providedIn: 'root' }) +export class CoreReportBuilderService { + + /** + * Obtain the reports list. + * + * @param page Current page. + * @param perpage Reports obtained per page. + * @returns Reports list. + */ + async getReports(page?: number, perpage?: number): Promise { + const site = CoreSites.getRequiredCurrentSite(); + const preSets: CoreSiteWSPreSets = { cacheKey: this.getReportBuilderCacheKey() }; + const response = await site.read( + 'core_reportbuilder_list_reports', + { page, perpage }, + preSets, + ); + + return response.reports; + } + + /** + * Get the detail of a report. + * + * @param reportid Report id + * @param page Current page. + * @param perpage Rows obtained per page. + * @returns Detail of the report. + */ + async loadReport(reportid: number, page?: number, perpage?: number): Promise { + const site = CoreSites.getRequiredCurrentSite(); + const preSets: CoreSiteWSPreSets = { cacheKey: this.getReportBuilderReportCacheKey() }; + const report = await site.read( + 'core_reportbuilder_retrieve_report', + { reportid, page, perpage }, + preSets, + ); + + if (!report) { + throw new CoreError('An error ocurred.'); + } + + const settingsData: { + // eslint-disable-next-line @typescript-eslint/naming-convention + cardview_showfirsttitle: number; + // eslint-disable-next-line @typescript-eslint/naming-convention + cardview_visiblecolumns: number; + } = report.details.settingsdata ? JSON.parse(report.details.settingsdata) : {}; + + const mappedSettingsData: CoreReportBuilderReportDetailSettingsData = { + cardviewShowFirstTitle: settingsData.cardview_showfirsttitle === 1, + cardviewVisibleColumns: settingsData.cardview_visiblecolumns ?? 1, + }; + + return { + ...report, + details: { + ...report.details, + settingsdata: mappedSettingsData, + }, + data: { + ...report.data, + rows: [...report.data.rows.map(row => ({ columns: row.columns, isExpanded: row.isExpanded ?? false }))], + }, + }; + } + + /** + * View a report. + * + * @param reportid Report viewed. + * @returns Response of the WS. + */ + async viewReport(reportid: string): Promise { + const site = CoreSites.getRequiredCurrentSite(); + + await site.write('core_reportbuilder_view_report', { reportid }); + } + + /** + * Check if the feature is enabled or disabled. + * + * @returns Feature enabled or disabled. + */ + async isEnabled(): Promise { + const site = CoreSites.getRequiredCurrentSite(); + const hasTheVersionRequired = site.isVersionGreaterEqualThan('4.1'); + const hasAdvancedFeatureEnabled = site.canUseAdvancedFeature('enablecustomreports'); + const isFeatureDisabled = site.isFeatureDisabled('CoreReportBuilderDelegate'); + + return hasTheVersionRequired && hasAdvancedFeatureEnabled && !isFeatureDisabled; + } + + /** + * Invalidates reports list WS calls. + * + * @returns Promise resolved when the list is invalidated. + */ + async invalidateReportsList(): Promise { + const site = CoreSites.getRequiredCurrentSite(); + await site.invalidateWsCacheForKey(this.getReportBuilderCacheKey()); + } + + /** + * Invalidates report WS calls. + * + * @returns Promise resolved when report is invalidated. + */ + async invalidateReport(): Promise { + const site = CoreSites.getCurrentSite(); + + if (!site) { + return; + } + + await site.invalidateWsCacheForKey(this.getReportBuilderReportCacheKey()); + } + + /** + * Get cache key for report builder list WS calls. + * + * @returns Cache key. + */ + protected getReportBuilderCacheKey(): string { + return ROOT_CACHE_KEY + 'list'; + } + + /** + * Get cache key for report builder report WS calls. + * + * @returns Cache key. + */ + protected getReportBuilderReportCacheKey(): string { + return ROOT_CACHE_KEY + 'report'; + } + + isString(value: unknown): boolean { + return typeof value === 'string'; + } + +} + +export const CoreReportBuilder = makeSingleton(CoreReportBuilderService); + +type CoreReportBuilderPagination = { + page?: number; + perpage?: number; +}; + +export type CoreReportBuilderRetrieveReportWSParams = CoreReportBuilderPagination & { + reportid: number; // Report ID. +}; + +/** + * Data returned by core_reportbuilder_list_reports WS. + */ +export type CoreReportBuilderListReportsWSResponse = { + reports: CoreReportBuilderReportWSResponse[]; + warnings?: CoreWSExternalWarning[]; +}; + +export type CoreReportBuilderReportWSResponse = { + name: string; // Name. + source: string; // Source. + type: number; // Type. + uniquerows: boolean; // Uniquerows. + conditiondata: string; // Conditiondata. + settingsdata: string | null; // Settingsdata. + contextid: number; // Contextid. + component: string; // Component. + area: string; // Area. + itemid: number; // Itemid. + usercreated: number; // Usercreated. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + sourcename: string; // Sourcename. + modifiedby: { + id: number; // Id. + email: string; // Email. + idnumber: string; // Idnumber. + phone1: string; // Phone1. + phone2: string; // Phone2. + department: string; // Department. + institution: string; // Institution. + fullname: string; // Fullname. + identity: string; // Identity. + profileurl: string; // Profileurl. + profileimageurl: string; // Profileimageurl. + profileimageurlsmall: string; // Profileimageurlsmall. + }; +}; + +/** + * Data returned by core_reportbuilder_retrieve_report WS. + */ +export type CoreReportBuilderRetrieveReportWSResponse = { + details: CoreReportBuilderReportWSResponse; + data: CoreReportBuilderReportDataWSResponse; + warnings?: CoreWSExternalWarning[]; +}; + +export interface CoreReportBuilderRetrieveReportMapped extends Omit { + details: CoreReportBuilderReportDetail; +} + +export type CoreReportBuilderReportDataWSResponse = { + headers: string[]; // Headers. + rows: { // Rows. + columns: (string | number)[]; // Columns. + isExpanded: boolean; + }[]; + totalrowcount: number; // Totalrowcount. +}; + +/** + * Params of core_reportbuilder_view_report WS. + */ +export type CoreReportBuilderViewReportWSParams = { + reportid: number; // Report ID. +}; + +/** + * Data returned by core_reportbuilder_view_report WS. + */ +export type CoreReportBuilderViewReportWSResponse = { + status: boolean; // Success. + warnings?: CoreWSExternalWarning[]; +}; + +export interface CoreReportBuilderReportDetail extends Omit { + settingsdata: CoreReportBuilderReportDetailSettingsData; +} + +export type CoreReportBuilderReportDetailSettingsData = { + cardviewShowFirstTitle: boolean; + cardviewVisibleColumns: number; +}; + +export interface CoreReportBuilderReport extends CoreReportBuilderReportWSResponse {}; diff --git a/src/core/features/reportbuilder/tests/behat/reportbuilder.feature b/src/core/features/reportbuilder/tests/behat/reportbuilder.feature new file mode 100644 index 000000000..fc004127d --- /dev/null +++ b/src/core/features/reportbuilder/tests/behat/reportbuilder.feature @@ -0,0 +1,146 @@ +@app @javascript @core_reportbuilder @lms_from4.1 +Feature: Report builder + + Background: + Given the Moodle site is compatible with this feature + And the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report 01 | core_user\reportbuilder\datasource\users | 1 | + | My report 02 | core_user\reportbuilder\datasource\users | 2 | + | My report 03 | core_user\reportbuilder\datasource\users | 3 | + | My report 04 | core_user\reportbuilder\datasource\users | 4 | + | My report 05 | core_user\reportbuilder\datasource\users | 5 | + | My report 06 | core_user\reportbuilder\datasource\users | 6 | + | My report 07 | core_user\reportbuilder\datasource\users | 7 | + | My report 08 | core_user\reportbuilder\datasource\users | 8 | + | My report 09 | core_user\reportbuilder\datasource\users | 9 | + | My report 10 | core_user\reportbuilder\datasource\users | 10 | + | My report 11 | core_user\reportbuilder\datasource\users | 11 | + | My report 12 | core_user\reportbuilder\datasource\users | 12 | + | My report 13 | core_user\reportbuilder\datasource\users | 13 | + | My report 14 | core_user\reportbuilder\datasource\users | 14 | + | My report 15 | core_user\reportbuilder\datasource\users | 15 | + | My report 16 | core_user\reportbuilder\datasource\users | 16 | + | My report 17 | core_user\reportbuilder\datasource\users | 17 | + | My report 18 | core_user\reportbuilder\datasource\users | 18 | + | My report 19 | core_user\reportbuilder\datasource\users | 19 | + | My report 20 | core_user\reportbuilder\datasource\users | 20 | + | My report 21 | core_user\reportbuilder\datasource\users | 21 | + | My report 22 | core_user\reportbuilder\datasource\users | 22 | + | My report 23 | core_user\reportbuilder\datasource\users | 23 | + | My report 24 | core_user\reportbuilder\datasource\users | 24 | + | My report 25 | core_user\reportbuilder\datasource\users | 25 | + | My report 26 | core_user\reportbuilder\datasource\users | 26 | + | My report 27 | core_user\reportbuilder\datasource\users | 27 | + | My report 28 | core_user\reportbuilder\datasource\users | 28 | + | My report 29 | core_user\reportbuilder\datasource\users | 29 | + | My report 30 | core_user\reportbuilder\datasource\users | 30 | + | My report 31 | core_user\reportbuilder\datasource\users | 31 | + | My report 32 | core_user\reportbuilder\datasource\users | 32 | + | My report 33 | core_user\reportbuilder\datasource\users | 33 | + | My report 34 | core_user\reportbuilder\datasource\users | 34 | + | My report 35 | core_user\reportbuilder\datasource\users | 35 | + And the following "core_reportbuilder > Columns" exist: + | report | uniqueidentifier | + | My report 01 | user:fullname | + | My report 02 | user:fullname | + | My report 03 | user:fullname | + | My report 04 | user:fullname | + | My report 05 | user:fullname | + | My report 06 | user:fullname | + | My report 07 | user:fullname | + | My report 08 | user:fullname | + | My report 09 | user:fullname | + | My report 10 | user:fullname | + | My report 11 | user:fullname | + | My report 12 | user:fullname | + | My report 13 | user:fullname | + | My report 14 | user:fullname | + | My report 15 | user:fullname | + | My report 16 | user:fullname | + | My report 17 | user:fullname | + | My report 18 | user:fullname | + | My report 19 | user:fullname | + | My report 20 | user:fullname | + | My report 21 | user:fullname | + | My report 22 | user:fullname | + | My report 23 | user:fullname | + | My report 24 | user:fullname | + | My report 25 | user:fullname | + | My report 26 | user:fullname | + | My report 27 | user:fullname | + | My report 28 | user:fullname | + | My report 29 | user:fullname | + | My report 30 | user:fullname | + | My report 31 | user:fullname | + | My report 32 | user:fullname | + | My report 33 | user:fullname | + | My report 34 | user:fullname | + | My report 35 | user:fullname | + And the following "core_reportbuilder > Audiences" exist: + | report | configdata | classname | + | My report 01 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 02 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 03 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 04 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 05 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 06 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 07 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 08 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 09 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 10 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 11 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 12 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 13 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 14 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 15 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 16 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 17 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 18 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 19 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 20 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 21 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 22 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 23 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 24 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 25 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 26 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 27 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 28 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 29 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 30 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 31 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 32 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 33 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 34 | | core_reportbuilder\reportbuilder\audience\allusers | + | My report 35 | | core_reportbuilder\reportbuilder\audience\allusers | + And the following "users" exist: + | username | firstname | lastname | email | city | + | student1 | Lionel | Smith | lionel@example.com | Bilbao | + + Scenario: Open report in mobile + Given I enter the app + And I log in as "student1" + And I press the user menu button in the app + When I press "Reports" in the app + + # Find report in the screen + Then I should find "My report 03" in the app + And I press "My report 03" in the app + And I should find "My report 03" in the app + And I should find "Lionel Smith" in the app + But I should not find "My report 02" in the app + + Scenario: Open report in tablet + Given I enter the app + And I change viewport size to "1200x640" + And I log in as "student1" + And I press the user menu button in the app + When I press "Reports" in the app + + # Find report in the screen + Then I should find "My report 02" in the app + And I press "My report 02" in the app + And I should find "My report 02" in the app + And I should find "Lionel Smith" in the app + But I should not find "My report 03" in the app diff --git a/src/core/features/settings/classes/settings-sections-source.ts b/src/core/features/settings/classes/settings-sections-source.ts index d3bcd02c9..caf755488 100644 --- a/src/core/features/settings/classes/settings-sections-source.ts +++ b/src/core/features/settings/classes/settings-sections-source.ts @@ -16,7 +16,7 @@ import { CoreConstants } from '@/core/constants'; import { Params } from '@angular/router'; import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module'; -import { CoreApp } from '@services/app'; +import { CorePlatform } from '@services/platform'; /** * Provides a collection of setting sections. @@ -45,7 +45,7 @@ export class CoreSettingsSectionsSource extends CoreRoutedItemsManagerSource -

{{ appName }} {{ versionName }}

+

{{ appName }} {{ versionName }}

diff --git a/src/core/features/settings/pages/dev/dev.html b/src/core/features/settings/pages/dev/dev.html index bf055c1ca..d1a0deffd 100644 --- a/src/core/features/settings/pages/dev/dev.html +++ b/src/core/features/settings/pages/dev/dev.html @@ -19,35 +19,35 @@ -

Text direction

+

Text direction

{{ direction }}

-

Force safe area margins

+

Force safe area margins

-

Enable remote styles {{remoteStylesCount}} -

+

Enable remote styles {{remoteStylesCount}} +

-

Enable site plugin styles {{pluginStylesCount}} -

+

Enable site plugin styles {{pluginStylesCount}} +

-

Reset user tours

+

Reset user tours

@@ -61,7 +61,7 @@ -

{{ feature }}

+

{{ feature }}

@@ -72,7 +72,7 @@ -

{{ plugin.addon }} ({{plugin.component}})

+

{{ plugin.addon }} ({{plugin.component}})

{{plugin.version}}

diff --git a/src/core/features/settings/pages/dev/dev.ts b/src/core/features/settings/pages/dev/dev.ts index 63be91d59..c7de23c24 100644 --- a/src/core/features/settings/pages/dev/dev.ts +++ b/src/core/features/settings/pages/dev/dev.ts @@ -13,8 +13,10 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; +import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreUserTours } from '@features/usertours/services/user-tours'; +import { CoreConfig } from '@services/config'; import { CorePlatform } from '@services/platform'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -149,6 +151,9 @@ export class CoreSettingsDevPage implements OnInit { */ async resetUserTours(): Promise { await CoreUserTours.resetTours(); + + await CoreConfig.delete(CoreLoginHelperProvider.ONBOARDING_DONE); + CoreDomUtils.showToast('User tours have been reseted'); } diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.html b/src/core/features/settings/pages/deviceinfo/deviceinfo.html index cd049841d..22ee220d5 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.html +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.html @@ -21,26 +21,27 @@ - {{ 'core.settings.developeroptions' | translate }} +

{{ 'core.settings.developeroptions' | translate }}

-

{{ 'core.settings.appversion' | translate}}

+

{{ 'core.settings.appversion' | translate}}

{{ deviceInfo.versionName }} ({{ deviceInfo.versionCode }})

-

{{ 'core.settings.compilationinfo' | translate }}

+

{{ 'core.settings.compilationinfo' | translate }}

{{ deviceInfo.compilationTime | coreFormatDate: "LLL Z": false }}

{{ deviceInfo.lastCommit }}

-

{{ 'core.settings.siteinfo' | translate }} * -

+

{{ 'core.settings.siteinfo' | translate }} + * +

{{ deviceInfo.siteUrl }}

{{ deviceInfo.siteVersion }}

{{ deviceInfo.siteId }}

@@ -48,7 +49,7 @@
-

{{ 'core.settings.filesystemroot' | translate }}

+

{{ 'core.settings.filesystemroot' | translate }}

{{ deviceInfo.fileSystemRoot }} @@ -59,97 +60,97 @@ -

{{ 'core.settings.navigatoruseragent' | translate }}

+

{{ 'core.settings.navigatoruseragent' | translate }}

{{ deviceInfo.userAgent }}

-

{{ 'core.settings.navigatorlanguage' | translate }}

+

{{ 'core.settings.navigatorlanguage' | translate }}

{{ deviceInfo.browserLanguage }}

-

{{ 'core.settings.currentlanguage' | translate }}

+

{{ 'core.settings.currentlanguage' | translate }}

{{ currentLangName }} ({{ deviceInfo.currentLanguage }})

-

{{ 'core.settings.locationhref' | translate }}

+

{{ 'core.settings.locationhref' | translate }}

{{ deviceInfo.locationHref }}

-

{{ 'core.settings.displayformat' | translate }}

+

{{ 'core.settings.displayformat' | translate }}

{{ 'core.' + deviceInfo.deviceType | translate }}

-

{{ 'core.settings.deviceos' | translate}}

+

{{ 'core.settings.deviceos' | translate}}

{{ deviceOsTranslated }}

-

{{ 'core.settings.screen' | translate }}

+

{{ 'core.settings.screen' | translate }}

{{ deviceInfo.screen }}

-

{{ 'core.settings.networkstatus' | translate}}

+

{{ 'core.settings.networkstatus' | translate}}

{{ 'core.' + deviceInfo.networkStatus | translate }}

-

{{ 'core.settings.wificonnection' | translate}}

+

{{ 'core.settings.wificonnection' | translate}}

{{ 'core.' + deviceInfo.wifiConnection | translate }}

-

{{ 'core.settings.cordovaversion' | translate }}

+

{{ 'core.settings.cordovaversion' | translate }}

{{ deviceInfo.cordovaVersion }}

-

{{ 'core.settings.cordovadeviceplatform' | translate }}

+

{{ 'core.settings.cordovadeviceplatform' | translate }}

{{ deviceInfo.platform }}

-

{{ 'core.settings.cordovadeviceosversion' | translate }}

+

{{ 'core.settings.cordovadeviceosversion' | translate }}

{{ deviceInfo.osVersion }}

-

{{ 'core.settings.cordovadevicemodel' | translate}}

+

{{ 'core.settings.cordovadevicemodel' | translate}}

{{ deviceInfo.model }}

-

{{ 'core.settings.cordovadeviceuuid' | translate}}

+

{{ 'core.settings.cordovadeviceuuid' | translate}}

{{ deviceInfo.uuid }}

-

{{ 'core.settings.pushid' | translate }}

+

{{ 'core.settings.pushid' | translate }}

{{ deviceInfo.pushId }}

-

{{ 'core.settings.localnotifavailable' | translate }}

+

{{ 'core.settings.localnotifavailable' | translate }}

{{ 'core.' + deviceInfo.localNotifAvailable | translate }}

diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts index 1ce2ff010..81d20558c 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreApp } from '@services/app'; import { Component, OnDestroy } from '@angular/core'; import { CoreConstants } from '@/core/constants'; import { CoreLocalNotifications } from '@services/local-notifications'; @@ -111,10 +110,10 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy { if (CorePlatform.isMobile()) { this.deviceInfo.deviceType = CorePlatform.is('tablet') ? 'tablet' : 'phone'; - if (CoreApp.isAndroid()) { + if (CorePlatform.isAndroid()) { this.deviceInfo.deviceOs = 'android'; this.deviceOsTranslated = 'Android'; - } else if (CoreApp.isIOS()) { + } else if (CorePlatform.isIOS()) { this.deviceInfo.deviceOs = 'ios'; this.deviceOsTranslated = 'iOS'; } else { diff --git a/src/core/features/settings/pages/general/general.html b/src/core/features/settings/pages/general/general.html index c3de108b3..9cc006e57 100644 --- a/src/core/features/settings/pages/general/general.html +++ b/src/core/features/settings/pages/general/general.html @@ -13,7 +13,7 @@ -

{{ 'core.settings.language' | translate }}

+

{{ 'core.settings.language' | translate }}

@@ -22,7 +22,7 @@
-

{{ 'core.settings.fontsize' | translate }}

+

{{ 'core.settings.fontsize' | translate }}

-

{{ 'core.settings.colorscheme' | translate }}

+

{{ 'core.settings.colorscheme' | translate }}

{{ 'core.settings.forcedsetting' | translate }}

-

{{ 'core.settings.enablerichtexteditor' | translate }}

+

{{ 'core.settings.enablerichtexteditor' | translate }}

{{ 'core.settings.enablerichtexteditordescription' | translate }}

-

{{ 'core.settings.ioscookies' | translate }}

+

{{ 'core.settings.ioscookies' | translate }}

{{ 'core.settings.ioscookiesdescription' | translate }}

{{ 'core.opensettings' | translate }} @@ -69,14 +69,14 @@
-

{{ 'core.settings.debugdisplay' | translate }}

+

{{ 'core.settings.debugdisplay' | translate }}

{{ 'core.settings.debugdisplaydescription' | translate }}

-

{{ 'core.settings.enablefirebaseanalytics' | translate }}

+

{{ 'core.settings.enablefirebaseanalytics' | translate }}

{{ 'core.settings.enablefirebaseanalyticsdescription' | translate }}

diff --git a/src/core/features/settings/pages/general/general.ts b/src/core/features/settings/pages/general/general.ts index d24b3cc2c..874028c8e 100644 --- a/src/core/features/settings/pages/general/general.ts +++ b/src/core/features/settings/pages/general/general.ts @@ -20,13 +20,13 @@ import { CoreLang } from '@services/lang'; import { CoreDomUtils } from '@services/utils/dom'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreSettingsHelper, CoreColorScheme, CoreZoomLevel } from '../../services/settings-helper'; -import { CoreApp } from '@services/app'; import { CoreIframeUtils } from '@services/utils/iframe'; import { Diagnostic, Translate } from '@singletons'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { AlertButton } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; +import { CorePlatform } from '@services/platform'; /** * Page that displays the general settings. @@ -81,7 +81,7 @@ export class CoreSettingsGeneralPage { this.colorSchemes.push(CoreColorScheme.LIGHT); this.selectedScheme = this.colorSchemes[0]; } else { - this.isAndroid = CoreApp.isAndroid(); + this.isAndroid = CorePlatform.isAndroid(); this.colorSchemes = CoreSettingsHelper.getAllowedColorSchemes(); this.selectedScheme = await CoreConfig.get(CoreConstants.SETTINGS_COLOR_SCHEME, CoreColorScheme.LIGHT); } diff --git a/src/core/features/settings/pages/space-usage/space-usage.html b/src/core/features/settings/pages/space-usage/space-usage.html index ec3524adb..0f5040f75 100644 --- a/src/core/features/settings/pages/space-usage/space-usage.html +++ b/src/core/features/settings/pages/space-usage/space-usage.html @@ -57,7 +57,7 @@ -

{{ 'core.settings.total' | translate }}

+

{{ 'core.settings.total' | translate }}

{{ totalSpaceUsage | coreBytesToSize }} diff --git a/src/core/features/sharedfiles/components/list-modal/list-modal.html b/src/core/features/sharedfiles/components/list-modal/list-modal.html index 682496ad4..8fe9bb42a 100644 --- a/src/core/features/sharedfiles/components/list-modal/list-modal.html +++ b/src/core/features/sharedfiles/components/list-modal/list-modal.html @@ -1,7 +1,7 @@ -

{{ title }}

+

{{ title }}

diff --git a/src/core/features/sharedfiles/services/handlers/settings.ts b/src/core/features/sharedfiles/services/handlers/settings.ts index a501481b3..bba96ad89 100644 --- a/src/core/features/sharedfiles/services/handlers/settings.ts +++ b/src/core/features/sharedfiles/services/handlers/settings.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module'; -import { CoreApp } from '@services/app'; +import { CorePlatform } from '@services/platform'; import { makeSingleton } from '@singletons'; /** @@ -33,7 +33,7 @@ export class CoreSharedFilesSettingsHandlerService implements CoreSettingsHandle * @returns Whether or not the handler is enabled on a site level. */ async isEnabled(): Promise { - return CoreApp.isIOS(); + return CorePlatform.isIOS(); } /** diff --git a/src/core/features/sharedfiles/services/handlers/upload.ts b/src/core/features/sharedfiles/services/handlers/upload.ts index a32da22fd..c0ff37195 100644 --- a/src/core/features/sharedfiles/services/handlers/upload.ts +++ b/src/core/features/sharedfiles/services/handlers/upload.ts @@ -19,7 +19,7 @@ import { CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult, } from '@features/fileuploader/services/fileuploader-delegate'; -import { CoreApp } from '@services/app'; +import { CorePlatform } from '@services/platform'; import { makeSingleton } from '@singletons'; import { CoreSharedFilesHelper } from '../sharedfiles-helper'; /** @@ -37,7 +37,7 @@ export class CoreSharedFilesUploadHandlerService implements CoreFileUploaderHand * @returns True or promise resolved with true if enabled. */ async isEnabled(): Promise { - return CoreApp.isIOS(); + return CorePlatform.isIOS(); } /** diff --git a/src/core/features/sharedfiles/services/sharedfiles-helper.ts b/src/core/features/sharedfiles/services/sharedfiles-helper.ts index c3ab8d4c3..4fa26d913 100644 --- a/src/core/features/sharedfiles/services/sharedfiles-helper.ts +++ b/src/core/features/sharedfiles/services/sharedfiles-helper.ts @@ -18,7 +18,6 @@ import { FileEntry } from '@ionic-native/file/ngx'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; import { CoreFileUploaderHandlerResult } from '@features/fileuploader/services/fileuploader-delegate'; -import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; @@ -49,7 +48,7 @@ export class CoreSharedFilesHelperProvider { * Initialize. */ initialize(): void { - if (!CoreApp.isIOS()) { + if (!CorePlatform.isIOS()) { return; } diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index b780c5565..62f5682f8 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -59,7 +59,7 @@ -

{{ 'core.courses.availablecourses' | translate}}

+

{{ 'core.courses.availablecourses' | translate}}

@@ -75,7 +75,7 @@ -

{{ 'core.courses.categories' | translate}}

+

{{ 'core.courses.categories' | translate}}

@@ -87,7 +87,7 @@ -

{{ 'core.courses.mycourses' | translate}}

+

{{ 'core.courses.mycourses' | translate}}

@@ -98,7 +98,7 @@ -

{{ 'core.courses.searchcourses' | translate}}

+

{{ 'core.courses.searchcourses' | translate}}

diff --git a/src/core/features/siteplugins/pages/module-index/module-index.ts b/src/core/features/siteplugins/pages/module-index/module-index.ts index 0dccf03fe..053eb374f 100644 --- a/src/core/features/siteplugins/pages/module-index/module-index.ts +++ b/src/core/features/siteplugins/pages/module-index/module-index.ts @@ -19,6 +19,9 @@ import { CoreCourseModuleData } from '@features/course/services/course-helper'; import { CanLeave } from '@guards/can-leave'; import { CoreNavigator } from '@services/navigator'; import { CoreSitePluginsModuleIndexComponent } from '../../components/module-index/module-index'; +import { CoreSites } from '@services/sites'; +import { CoreFilterFormatTextOptions } from '@features/filter/services/filter'; +import { CoreFilterHelper } from '@features/filter/services/filter-helper'; /** * Page to render the index page of a module site plugin. @@ -38,10 +41,31 @@ export class CoreSitePluginsModuleIndexPage implements OnInit, CanLeave { /** * @inheritdoc */ - ngOnInit(): void { + async ngOnInit(): Promise { this.title = CoreNavigator.getRouteParam('title'); this.module = CoreNavigator.getRouteParam('module'); this.courseId = CoreNavigator.getRouteNumberParam('courseId'); + + if (this.title) { + const siteId = CoreSites.getCurrentSiteId(); + + const options: CoreFilterFormatTextOptions = { + clean: false, + courseId: this.courseId, + wsNotFiltered: false, + singleLine: true, + }; + + const filteredTitle = await CoreFilterHelper.getFiltersAndFormatText( + this.title.trim(), + 'module', + this.module?.id ?? -1, + options, + siteId, + ); + + this.title = filteredTitle.text; + } } /** diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 6cc006cd4..77247e9b7 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -97,7 +97,7 @@ export class CoreSitePluginsProvider { }; if (args.appismobile) { - defaultArgs.appplatform = CoreApp.isIOS() ? 'ios' : 'android'; + defaultArgs.appplatform = CorePlatform.isIOS() ? 'ios' : 'android'; } return { diff --git a/src/core/features/tag/components/feed/core-tag-feed.html b/src/core/features/tag/components/feed/core-tag-feed.html index 9e8f44309..17b3de5ca 100644 --- a/src/core/features/tag/components/feed/core-tag-feed.html +++ b/src/core/features/tag/components/feed/core-tag-feed.html @@ -6,7 +6,7 @@ -

{{ item.heading }}

+

{{ item.heading }}

{{ text }}

diff --git a/src/core/features/tag/pages/index/index.html b/src/core/features/tag/pages/index/index.html index 151b3f93f..2a07a4074 100644 --- a/src/core/features/tag/pages/index/index.html +++ b/src/core/features/tag/pages/index/index.html @@ -21,13 +21,13 @@ {{ 'core.tag.warningareasnotsupported' | translate }}
+ (click)="openArea(area)" [attr.aria-current]="area.id == selectedAreaId ? 'page' : 'false'" button detail="true"> -

{{ area!.nameKey | translate }}

+

{{ area.nameKey | translate }}

- - - {{ 'core.tag.tagareabadgedescription' | translate:{ count: area!.badge } }} + + + {{ 'core.tag.tagareabadgedescription' | translate:{ count: area.badge } }}
diff --git a/src/core/features/user/components/tag-area/core-user-tag-area.html b/src/core/features/user/components/tag-area/core-user-tag-area.html index 3531973e0..ccba8b626 100644 --- a/src/core/features/user/components/tag-area/core-user-tag-area.html +++ b/src/core/features/user/components/tag-area/core-user-tag-area.html @@ -2,7 +2,7 @@ -

{{ item.heading }}

+

{{ item.heading }}

diff --git a/src/core/features/user/pages/about/about.html b/src/core/features/user/pages/about/about.html index 751d3dd52..bf0f04d81 100644 --- a/src/core/features/user/pages/about/about.html +++ b/src/core/features/user/pages/about/about.html @@ -32,12 +32,12 @@ -

{{ 'core.user.contact' | translate}}

+

{{ 'core.user.contact' | translate}}

-

{{ 'core.user.email' | translate }}

+

{{ 'core.user.email' | translate }}

{{ user.email }}

@@ -45,7 +45,7 @@
-

{{ 'core.user.phone1' | translate}}

+

{{ 'core.user.phone1' | translate}}

{{ user.phone1 }}

@@ -53,7 +53,7 @@
-

{{ 'core.user.phone2' | translate}}

+

{{ 'core.user.phone2' | translate}}

{{ user.phone2 }}

@@ -61,7 +61,7 @@
-

{{ 'core.user.address' | translate}}

+

{{ 'core.user.address' | translate}}

{{ formattedAddress }}

@@ -69,13 +69,13 @@
-

{{ 'core.user.city' | translate}}

+

{{ 'core.user.city' | translate}}

{{ user.city }}

-

{{ 'core.user.country' | translate}}

+

{{ 'core.user.country' | translate}}

{{ user.country }}

@@ -83,12 +83,12 @@ -

{{ 'core.userdetails' | translate}}

+

{{ 'core.userdetails' | translate}}

-

{{ 'core.user.webpage' | translate}}

+

{{ 'core.user.webpage' | translate}}

{{ user.url }}

@@ -96,7 +96,7 @@