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